let appConfig = {
    // host: 'http://sz.emtailor.com:8068/',
    host: 'http://localhost:8068/',
    // host: 'http://e.7mo.org:8068/',
    /*
    客户端类型: client,middle,file
    client: 1.客户端模式,不进行心跳;
    middle: 1.中间件模式,进行心跳;
    file:   1.文件端模式,不进行心跳;
    one:    4.一体端模式,不进行心跳;
     */
    serviceType: 'one',
    /*
    是否debug模式: true,false
    影响一些日志的输出
     */
    isDebug: false,
    /*
    是否全局结束所有任务,默认false
     */
    allStop: false,
    /*
    axios全局超时时间ms
     */
    axiosTimeOut: 60 * 1000,
    /*
    心跳惩罚时间秒
     */
    heartBeatPunish: 10,
    heartBeatFixedDelay: 10,
    /*
    用户解封时间ms
     */
    userUnBanIntervalMs: 172800000,
    /*
    默认任务间隔ms
     */
    defaultSleepTaskMs: 86400000,
    defaultUserCheckUrl: 'https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=4143607182&first=40',
    /*
    忽略图片下载失败
     */
    ignoreGetImage: true,
    /*
    是否健康检查(自动启动任务)
     */
    isHealthCheck: true,
    /*
    健康检查周期(阀值,超过此时间将尝试启动任务)
     */
    healthCheckIntervalMs: 10 * 60 * 1000,
    /*
    邮件告警间隔(触发第多少次发送邮件,才发送邮件;不要让邮件发送太频繁,添加容错范围)
     */
    emailCountIgnore: 6,
};
String.prototype.reverse = function () {
    return this.split('').reverse().join('')
};

String.prototype.toSize = function () {
    let size = this;
    if (!!size && size > 0) {
        const KB = 1024;
        const MB = 1024 * KB;
        const GB = 1024 * MB;
        const TB = 1024 * GB;
        let sizeStr = size > TB ? size / TB + "TB" : size > GB ? size / GB + "GB" : size > MB ? size / MB + "MB" : size > KB ? size / KB + "KB" : size;
        let unit = sizeStr.substr(-2);
        let number = sizeStr.replace(unit, '');
        number = Math.floor(number * 10) / 10;
        sizeStr = number + unit;
        return sizeStr;
    } else {
        return '0KB';
    }
};

String.prototype.between = function (start = '', end = '', isInner = true) {
    let str = this;
    if (!!str && str.length > 0) {
        let startIndex = 0;
        if (start) {
            let io = str.indexOf(start);
            if (io >= 0) {
                if (isInner) {
                    io = io + start.length;
                }
                startIndex = io;
            } else {
                return '';
            }
        }
        let endIndex = str.length;
        if (end) {
            let io = str.indexOf(end, startIndex);
            if (io >= 0) {
                if (!isInner) {
                    io = io + end.length;
                }
                endIndex = io;
            } else {
                return '';
            }
        }
        return str.substring(startIndex, endIndex);
    } else {
        return '';
    }
};

let UUID = (len) => {
    let max = 520 << 520;
    len = len === null || isNaN(len) || len > max ? 6 : len;
    let uuid = '';
    let rand = () => ('' + Math.random()).substring(2);
    do {
        uuid += rand();
    } while (uuid.length < len) ;
    return uuid.substring(0, len);
};

let trimQuote = (str) => {
    let trimStr = String(str).trim();
    if (!!trimStr) {
        let quote = '"';
        if (trimStr.startsWith(quote)) {
            trimStr = trimStr.substr(1);
        }
        if (trimStr.endsWith(quote)) {
            trimStr = trimStr.substring(0, trimStr.lastIndexOf(quote));
        }
    }
    return trimStr;
};

/**
 * 获取字符串的 哈希值
 * @param str 字符串
 * @param caseSensitive 区分大小写
 * @returns {number}
 */
let getHashCode = (str, caseSensitive = true) => {
    str = '' + str;
    if (!caseSensitive) {
        str = str.toLowerCase();
    }
    let hash = 13709061665, i, ch;
    for (i = str.length - 1; i >= 0; i--) {
        ch = str.charCodeAt(i);
        hash ^= ((hash << 5) + ch + (hash >> 2));
    }
    return (hash & 2147483647);
};

/**
 * fake flat for webStorm,idea
 * @type {string}
 */
let fakeU = 'fake webStorm,idea';
if (!fakeU) {
    Array.prototype.flat = () => Array.prototype.flat();
    /**
     *
     * @param url{string}
     * @param param{object}
     * @returns {object}
     */
    let axiosParam = (url, param = {}) => Object.assign(url, param);
    /**
     *
     * @type {{request: (function(string, Object): string & Object), post: (function(string, Object): string & Object), get: (function(string, Object): string & Object), delete: (function(string, Object): string & Object), put: (function(string, Object): string & Object)}}
     */
    axios = {
        get: axiosParam,
        put: axiosParam,
        post: axiosParam,
        delete: axiosParam,
        request: axiosParam,
    };
    Vue = {
        getCurrentInstance: () => 0,
        // @see <a href="https://blog.csdn.net/weixin_34370347/article/details/89617464">Vue获取组件name属性</a>
        $options: {},
        globalProperties: {},
        $emit: () => 0,
        mount: () => 0,
        unmount: () => 0,
        createApp: () => 0,
        reactive: () => 0,
        $el: {},
    };
    /**
     * js-sha1
     * @see https://github.com/emn178/js-sha1
     * @param str
     * @returns {string}
     */
    sha1 = (str) => '';
    VueRouter = {
        createRouter: () => 0,
        createWebHashHistory: () => 0,
    };
    /**
     * js 代码格式化工具
     * @link https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.11.0/beautify.min.js
     * @param jsCode js脚本文本
     * @returns {string}
     */
    js_beautify = (jsCode) => String(jsCode);
}

/**
 * 验证脚本合法性
 * @returns {boolean}
 */
String.prototype.validScript = function () {
    let scriptStrObj = this;
    try {
        // 这里要转换为字符串才能正确验证, 否则传入的是String()对象, 无法验证
        eval(String(scriptStrObj));
        return true;
    } catch (e) {
        console.error('无效的脚本: %o, error: {}', scriptStrObj, e);
    }
    return false;
};

/**
 *  禁止用F5键
 * @returns {boolean}
 */
document.onkeydown = () => {
    console.log('event.keyCode', event.keyCode);
    if (event.keyCode === 116) {
        //event.keyCode = 0;
        //event.cancelBubble = true;
        //// window.location.href = '/';
        // return false;
    }
};

/**
 * 从数组中的对象中查找key键名对应的值包含被查找的值数组
 * @param sourceArr 源对象数组
 * @param key 数组中对象的key键名
 * @param valuesArr key的值数组
 * @returns {*}
 */
let findKey = (sourceArr, key, valuesArr) => sourceArr.filter(e => valuesArr.filter(f => f === e[key]).length === 1);

let randNumber = (min, max) => parseInt(Math.random() * (max - min + 1) + min, 10);

let deepClone = (obj) => JSON.parse(JSON.stringify(obj));

/**
 * 日期转指定格式字符串
 * @param fmt 日期格式化函数
 * @returns {string} 格式化后的日期字符串
 * @modified zhongbo
 * @date 2020/5/22
 */
Date.prototype.fmt = function dateFormat(fmt = 'MM月dd日HH:mm:ss,SSS') {
    //eg: yyyy-MM-dd HH:mm:ss
    let date = this;
    let ret;
    let opt = {
        "y+": date.getFullYear().toString(),        // 年
        "Y+": date.getFullYear().toString(),        // 年
        "M+": (date.getMonth() + 1).toString(),     // 月
        "d+": date.getDate().toString(),            // 日
        "D+": date.getDate().toString(),            // 日
        "H+": date.getHours().toString(),           // 时
        "m+": date.getMinutes().toString(),         // 分
        "s+": date.getSeconds().toString(),          // 秒
        "S+": date.getMilliseconds().toString()          // 秒
        // 有其他格式化字符需求可以继续添加，必须转化成字符串
    };
    for (let k in opt) {
        ret = new RegExp("(" + k + ")").exec(fmt);
        if (ret) {
            fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
        }
    }
    return fmt;
};

/**
 * 秒转时间字符串
 * @param second
 * @returns {string}
 */
let timeUnit = (second) => {
    if (!!Math.floor(second)) {
        const MINUTE = 60;
        const HOUR = 60 * MINUTE;
        const DAY = 24 * HOUR;
        let timeStr = second > DAY ? second / DAY + '天' : second > HOUR ? second / HOUR + '时' : second > MINUTE ? second / MINUTE + '分' : second + '秒';
        let unit = timeStr.substr(-1);
        let number = timeStr.replace(unit, '');
        number = Math.floor(number * 10) / 10;
        timeStr = number + unit;
        return timeStr;
    } else {
        return '0秒'
    }
};

/**
 * 按键事件检测
 *
 * 特殊按键说明
 * metaKey : 即Win键
 * ctrlKey : 即Ctrl键
 * altKey : 即Alt键
 * shiftKey : 即Shift键
 * 'Tab' : 即Tab键
 * 'Escape' : 即Esc键
 * @param {KeyboardEvent} event
 * @param {array} param
 * @return {boolean}
 */
let isEventKey = (event, param = []) => {
    let matchAllKey = true;
    // 判断 event 按键事件需要使用 event.nativeEvent 来判断事件类型
    if (!!event && !!param && event.nativeEvent instanceof KeyboardEvent && param instanceof Array) {
        param.forEach(key => {
            if (matchAllKey) {
                if ('ctrl' === key) {
                    matchAllKey = event.ctrlKey;
                } else if ('alt' === key) {
                    matchAllKey = event.altKey;
                } else {
                    matchAllKey = event.key === key;
                }
            }
        })
    } else {
        matchAllKey = false;
    }
    return matchAllKey;
};

/**
 * 字符串中间插入文本
 * @param sourceStr
 * @param start
 * @param insertStr
 * @return {*}
 */
let insertStr = (sourceStr, start, insertStr) => {
    if (!!sourceStr && !!insertStr && !isNaN(start) && sourceStr instanceof String && insertStr instanceof String) {
        return sourceStr.slice(0, start) + insertStr + sourceStr.slice(start);
    }
    return sourceStr;
};

/**
 * 数组中间插入内容
 * @param {array} sourceArr
 * @param {number} start
 * @param {*} insertObj
 * @return {*}
 */
let insertArr = (sourceArr, start, insertObj) => {
    if (!!sourceArr && !!insertObj) {
        sourceArr = _.clone(sourceArr);
        if (sourceArr instanceof Array) {
            // 判断超越最大下标
            if (sourceArr[start] === undefined) {
                sourceArr.push(insertObj);
            } else {
                // 中间添加一行
                sourceArr = sourceArr.map((v, i) => i === start ? [v, insertObj] : v).flat();
            }
        }
    }
    return sourceArr;
};

/**
 * 数组中间插入内容
 * @param {array} sourceArr
 * @param {number} start
 * @return {*}
 */
let removeArr = (sourceArr, start) => {
    if (!!sourceArr && typeof start === 'number') {
        sourceArr = _.clone(sourceArr);
        if (sourceArr instanceof Array) {
            // 中间删除一行
            sourceArr = sourceArr.filter((url, index) => index !== start);
        }
    } else {
        console.log('移除数组错误! 参数错误 sourceArr: %o start: %o', sourceArr, start);
    }
    return sourceArr;
};

/**
 * 字节转字符串
 * @param byteSize
 * @returns {string}
 */
let byteUnit = (byteSize) => {
    if (!!Math.floor(byteSize)) {
        const KB = 1024;
        const MB = 1024 * KB;
        const GB = 1024 * MB;
        let timeStr = byteSize > GB ? byteSize / GB + 'GB' : byteSize > MB ? byteSize / MB + 'MB' : byteSize > KB ? byteSize / KB + 'KB' : byteSize + 'B';
        let unit = timeStr.substr(-1);
        let unitHigh = timeStr.substr(-2, 1);
        unit = unitHigh > '9' ? unitHigh + unit : unit;
        let number = timeStr.replace(unit, '');
        number = Math.floor(number * 10) / 10;
        timeStr = number + unit;
        return timeStr;
    } else {
        return '0B'
    }
};

/**
 * 判断是否管理员
 * @return {boolean}
 */
let isAdmin = () => !!localStorage.getItem('token');

/**
 * 判断字符串是否为json
 * @param str
 * @returns {boolean}
 * @see <a href="https://www.cnblogs.com/lanleiming/p/7096973.html">【最简单的方法】js判断字符串是否为JSON格式（20180115更新）</a>
 */
let isJson = (str) => {
    if (typeof str === 'string') {
        try {
            let obj = JSON.parse(str);
            return !!(typeof obj === 'object' && obj);
        } catch (e) {
            console.log('isJson error： %o !!!', str, e);
        }
    }
    return false;
};

/**
 * 提取日志打印FormData信息
 * @param params 请求参数
 * @returns {object}
 */
let extractFormData = (params) => FormData && params && params instanceof FormData ? (() => {
    let next, returnObj = {}, pk = params.keys();
    while ((next = pk.next()) && !next.done) returnObj[next.value] = params.getAll(next.value);
    return returnObj;
})() : params;

//console.__proto__.blue = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:white;line-height:20px;', new Date().fmt(), ...e)
//};
//
//console.__proto__.green = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:blue;line-height:20px;', new Date().fmt(), ...e)
//};
//
//console.__proto__.yellow = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:yellow;line-height:20px;', new Date().fmt(), ...e)
//};
//
//console.__proto__.green = {
//    log: (...e) => console.log('%c[ %s ] Proxy' + e.map((e) => typeof(e) === 'object' ? '%o' : '%s').join(' ')
//        , 'background-color:blue;color:green;line-height:20px;', new Date().fmt(), ...e)
//};

/**
 * 编码转换
 *
 * 使用示例:
 * let str1 = charConvert('{"code":0,"msg":"æ\x93\x8Dä½\x9Cæ\x88\x90å\x8A\x9F","data":[]}', 'latin1', 'utf8')
 * let str1 = charConvert(str1,'utf-8','latin1')
 *
 * https://github.com/polygonplanet/encoding.js
 * https://unpkg.com/encoding-japanese@2.0.0/encoding.min.js
 *
 * @param str 字符串
 * @param from 原编码
 * @param to 目标编码
 * @returns {string|*} 转换后的字符串
 */
let charConvert = (str, from, to) => {
    if (typeof str !== 'string') return str;
    if (typeof Encoding === 'undefined') {
        console.log('encoding.js is not found!');
        return str;
    }
    let arrFrom = Encoding.stringToCode(str, from);
    let arrTo = Encoding.convert(arrFrom, from, to);
    return Encoding.codeToString(arrTo, to);
};

/**
 * Blob转换为base64互相转换
 * Blob序列化, 反序列化工具
 *
 * @see <a href="https://github.com/angadn/blob64">blob64</a>
 * @type {{serialize(*, *): void, deserialize(*): Blob}}
 */
let blob64 = {
    /**
     * 序列化
     * @param blob  Blob对象
     * @param onFinish 完成回调
     */
    serialize(blob, onFinish) {
        let reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.onloadend = function () {
            onFinish(reader.result);
        };
    },
    /**
     * 序列化 Promise版
     * 推荐使用
     *
     * @param blob  Blob对象
     * @returns {Promise<String>} 序列化后的字符串
     */
    serializePromise(blob) {
        return new Promise(resolve => {
            this.serialize(blob, resolve);
        });
    },
    /**
     * 反序列化
     * @param str base64字符串
     * @returns {Blob} Blob对象
     */
    deserialize(str) {
        let indexOfComma = str.lastIndexOf(",");
        let contentType = str.substr(0, indexOfComma + 1) || "";
        let byteChars = atob(str.substr(indexOfComma + 1));
        let sliceSize = 512;
        let byteArrays = [];
        for (let offset = 0; offset < byteChars.length; offset += sliceSize) {
            let slice = byteChars.slice(offset, offset + sliceSize);
            let byteNumbers = new Array(slice.length);
            for (let i = 0; i < slice.length; i++) {
                byteNumbers[i] = slice.charCodeAt(i);
            }
            byteArrays.push(new Uint8Array(byteNumbers));
        }
        return new Blob(byteArrays, {type: contentType});
    },
    /**
     * 反序列化 Promise版
     * 推荐使用
     *
     * @see <a href="https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript">stackoverflow</a>  * @param str base64字符串
     * @returns {Blob} Blob对象
     */
    deserializePromise: (str) => fetch(str).then(res => res.blob()),
    /**
     * 文本转换为Blob对象
     *
     * 常用: text/plain, text/html, text/html;charset=charset=utf-8;,
     * application/octet-stream, text/css, text/javascript, application/javascript,
     * application/json, application/xml, application/x-javascript, text/xml,
     * application/xhtml+xml, application/x-www-form-urlencoded, multipart/form-data
     *
     * @see <a href="https://github.com/kevin-roark/get-text-blob">get-text-blob</a>
     * @param text 文本
     * @param mimeType MIME类型, 默认text/plain
     * @returns {null} Blob对象
     */
    textToBlob(text, mimeType = 'text/plain') {
        let blob = null;
        try {
            if (text.startsWith('data:')) {
                blob = this.deserialize(text);
            } else {
                blob = new Blob([text], {type: mimeType});
            }
        } catch (e) {
            // TypeError old chrome and FF
            let blobBuilder = window['BlobBuilder'] ||
                window['WebKitBlobBuilder'] ||
                window['MozBlobBuilder'] ||
                window['MSBlobBuilder'];
            if (blobBuilder) {
                let bb = new blobBuilder();
                bb.append([text]);
                blob = bb['getBlob']('text/html');
            }
        }
        return blob;
    },
    /**
     * Blob对象转换为文本
     *
     * @see <a href="https://blog.csdn.net/qq_46090071/article/details/124199827">js在blob对象中读取文字内容</a>
     * @param blob Blob对象
     * @param charSet 字符集
     * @returns {Promise<String>} 文本
     */
    blobToTextPromise(blob, charSet = null) {
        return new Promise(resolve => {
            let reader = new FileReader();
            reader.onloadend = function () {
                resolve(reader.result);
            };
            if (charSet) {
                reader.readAsText(blob, charSet);
            } else {
                reader.readAsText(blob);
            }
        });
    },
    /**
     * 下载Blob对象到文件
     *
     * @see <a href="https://pixijs.io/examples/#/demos-advanced/screenshot.js">screenshot</a>
     * @param blob Blob对象
     * @param fileName 文件名
     */
    downLoadBlob(blob, fileName) {
        let a = document.createElement('a');
        a.href = window.URL.createObjectURL(blob);
        a.download = fileName;
        a.click();
        window.URL.revokeObjectURL(a.href);
    },
    /**
     * 下载Blob对象到文件
     *
     * @see <a href="https://pixijs.io/examples/#/demos-advanced/screenshot.js">screenshot</a>
     * @param blob Blob对象
     * @param fileName 文件名
     */
    downBlob(blob, fileName = "download") {
        const a = document.createElement('a');
        document.body.append(a);
        a.download = fileName;
        a.href = URL.createObjectURL(blob);
        a.click();
        a.remove();
        window.URL.revokeObjectURL(a.href);
    },
}

/**
 * 禁用调试模式
 * @see <a href="https://zmingcx.com/wp-content/cache/autoptimize/js/autoptimize_e7a8a795e88cf81ddfa417af57368243.js">提取自源码forbidDebug()</a>
 */
let forbidDebug = function () {
    // 是否允许调试
    let enableDebug = true;
    if (enableDebug) {
        return;
    }
    try {
        ((function () {
            let callbacks = [], timeLimit = 50, open = false;
            setInterval(loop, 1);
            return {
                addListener: function (fn) {
                    callbacks.push(fn)
                }, cancleListenr: function (fn) {
                    callbacks = callbacks.filter(function (v) {
                        return v !== fn
                    })
                }
            };

            function loop() {
                let startTime = new Date();
                //de//bugger;
                if (new Date() - startTime > timeLimit) {
                    if (!open) {
                        callbacks.forEach(function (fn) {
                            fn.call(null)
                        })
                    }
                    open = true;
                    window.stop();
                    alert("\u5173\u95ed\u63a7\u5236\u53f0\u540e\u5237\u65b0\uff01");
                    document.body.innerHTML = ""
                } else {
                    open = false
                }
            }
        })()).addListener(function () {
            window.location.reload()
        })
    } catch (e) {
    }
};
forbidDebug();

/**
 * 自制禁用调试
 */
(() => {
    // 是否启用
    let enable = false;
    let c = String.fromCharCode;
    let d = [40, 40, 41, 61, 62, 123, 100, 101, 98, 117, 103, 103, 101, 114, 125, 41, 40, 41];
    try {
        enable && eval(d.map(e => c(e)).join(''));
    } catch (e) {
        console.error(e);
        window.stop();
    }
})();

let apis = {
    style: {
        getApp: {
            // url: 'http://127.0.0.1:3003/src/css/app.css',
            url: 'src/css/app.css',
            method: 'get'
        }
    },
    swagger: {
        apiDocs: {
            url: 'v3/api-docs',
            method: 'get'
        },
    },
    task: {
        getOne: {
            url: 'Task/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Task/findAll',
            method: 'get'
        },
        add: {
            url: 'Task',
            method: 'post'
        },
        update: {
            url: 'Task',
            method: 'put'
        },
        delete: {
            url: 'Task/${id}',
            method: 'delete'
        },
        start: {
            url: 'Task/${id}/start',
            method: 'get'
        },
        stop: {
            url: 'Task/${id}/stop',
            method: 'get'
        },
        pause: {
            url: 'Task/${id}/pause',
            method: 'get'
        },
        resume: {
            url: 'Task/${id}/resume',
            method: 'get'
        },
        test: {
            url: 'Task/test',
            method: 'post'
        }
    },
    taskFar: {
        getOne: {
            url: 'TaskFar/${id}',
            method: 'get'
        },
        getAll: {
            url: 'TaskFar/findAll',
            method: 'get'
        },
        add: {
            url: 'TaskFar',
            method: 'post'
        },
        update: {
            url: 'TaskFar',
            method: 'put'
        },
        delete: {
            url: 'TaskFar/${id}',
            method: 'delete'
        },
        start: {
            url: 'TaskFar/${id}/start',
            method: 'get'
        },
        stop: {
            url: 'TaskFar/${id}/stop',
            method: 'get'
        },
        pause: {
            url: 'TaskFar/${id}/pause',
            method: 'get'
        },
        resume: {
            url: 'TaskFar/${id}/resume',
            method: 'get'
        },
        test: {
            url: 'TaskFar/test',
            method: 'post'
        }
    },
    proxy: {
        getOne: {
            url: 'Proxy/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Proxy/findAll',
            method: 'get'
        },
        add: {
            url: 'Proxy',
            method: 'post'
        },
        update: {
            url: 'Proxy',
            method: 'put'
        },
        delete: {
            url: 'Proxy/${id}',
            method: 'delete'
        }
    },
    config: {
        getOne: {
            url: 'Config/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Config/findAll',
            method: 'get'
        },
        add: {
            url: 'Config',
            method: 'post'
        },
        update: {
            url: 'Config',
            method: 'put'
        },
        delete: {
            url: 'Config/${id}',
            method: 'delete'
        }
    },
    insCrack: {
        getOne: {
            url: 'Crack/${id}',
            method: 'get'
        },
        getAll: {
            url: 'Crack/findAll',
            method: 'get'
        },
        add: {
            url: 'Crack',
            method: 'post'
        },
        update: {
            url: 'Crack',
            method: 'put'
        },
        delete: {
            url: 'Crack/${id}',
            method: 'delete'
        }
    },
    user: {
        getOne: {
            url: 'User/${id}',
            method: 'get'
        },
        getAll: {
            url: 'User/findAll',
            method: 'get'
        },
        add: {
            url: 'User',
            method: 'post'
        },
        update: {
            url: 'User',
            method: 'put'
        },
        delete: {
            url: 'User/${id}',
            method: 'delete'
        }
    },
    notifyEmail: {
        getOne: {
            url: 'NotifyEmail/${id}',
            method: 'get'
        },
        getAll: {
            url: 'NotifyEmail/findAll',
            method: 'get'
        },
        add: {
            url: 'NotifyEmail',
            method: 'post'
        },
        update: {
            url: 'NotifyEmail',
            method: 'put'
        },
        delete: {
            url: 'NotifyEmail/${id}',
            method: 'delete'
        }
    },
    eMail: {
        send: {
            url: 'Email',
            method: 'post',
            param_example: {content: "发送一封邮件", title: "swagger测试", to: "ni81@qq.com"},
        },
    },
    ins: {
        count: {
            url: 'Ins/user/count',
            method: 'get'
        },
        current: {
            url: 'Ins/user/current',
            method: 'get'
        },
        move: {
            url: 'Ins/user/move',
            method: 'get'
        },
        next: {
            url: 'Ins/user/next',
            method: 'get'
        },
        reset: {
            url: 'Ins/user/reset',
            method: 'get'
        },
    },
    post: {
        getOne: {
            url: 'Ins/post/${id}',
            method: 'get'
        },
        getPage: {
            url: 'Ins/post',
            method: 'get'
        },
        addOrUpdate: {
            url: 'Ins/post',
            method: 'post'
        },
        delete: {
            url: 'Ins/post/${id}',
            method: 'delete'
        },
        deleteMany: {
            url: 'Ins/post/delete',
            method: 'delete'
        },
        count: {
            url: 'Ins/post/count',
            method: 'get'
        },
    },
    middle: {
        heartBeat: {
            url: 'middle/ping',
            method: 'get'
        },
        lastHeartBeat: {
            url: 'middle/lastHeart',
            method: 'get'
        },
    },
    file: {
        listJson: {
            url: 'file/json',
            method: 'get'
        },
        list: {
            url: 'file',
            method: 'get'
        },
        delete: {
            // file?del=<String fileName>
            url: 'file?del=${file}',
            method: 'get',
            param_example: {file: 'aaa.txt0.8008880603584201'}
        },
        getOne: {
            // file?get=<String fileName>
            url: 'file?get=${file}',
            method: 'get',
            param_example: {file: 'aaa.txt0.8008880603584201'},
            config: {
                responseType: 'blob'
            },
        },
        upload: {
            url: 'file',
            method: 'post',
            param_example: '/*FormData*/tmp=await api.file.getOne({file:\'aaa.jpg\'});fd=new FormData();fd.append(\'file\',tmp.data,\'test.jpg\'+Math.random());up=await api.file.upload(fd)',
            config: {
                overrideMimeType: 'multipart/form-data'
            },
            info: '参数格式为 FormData'
        },
    },
    gram: {
        // https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=1001283596&first=40
        getPost: {
            url: 'https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=${id}&first=40',
            method: 'get',
            param_example: {id: '1001283596'}
        },
        // https://www.instagram.com/graphql/query/?query_hash=8c2a529969ee035a5063f2fc8602a0fd&variables={"id":"51132914782","first":12}
        getPostNew: {
            url: 'https://www.instagram.com/graphql/query/?query_hash=${queryHash}&variables={"id":"${id}","first":40}',
            method: 'get',
            queryHash: true,
            // 多图id: '51132914782'
            // 多图__typename: 'GraphSidecar'
            param_example: {id: '1001283596'}
        },
        getJpeg: {
            url: 'http${url}',
            method: 'get',
            param_example: {url: '1001283596'},
            usage_example: 'await api.gram.getJpeg({url:decodeURIComponent(\'https://scontent-hkt1-1.cdninstagram.com/v/t51.2885-15/sh0.08/e35/c0.180.1440.1440a/s640x640/271796139_918243432220319_6606270248120166088_n.jpg?_nc_ht=scontent-hkt1-1.cdninstagram.com\u0026_nc_cat=111\u0026_nc_ohc=2KPbEtGsfIIAX9MVvN3\u0026edm=APU89FABAAAA\u0026ccb=7-4\u0026oh=00_AT9LlzKJwZxDGjaZyEI4ov4XEGonwuvdU0xr2oAZ4kCgWA\u0026oe=61EC5D1C\u0026_nc_sid=86f79a\').substr(4)})',
            config: {
                responseType: 'blob'
            },
        },
    },
    far: {
        getTest: {
            url: '/hk/shopping/women/dion-lee-corset-detail-minidress-item-17005926.aspx',
            method: 'get',
            example: `.between('content="',' minidress',true)==='Shop Dion Lee corset-detail'`
        },
    },
    mongo: {
        save: {
            url: 'Mongo/post/${name}',
            method: 'post',
            example: `{id: '1001283596'}`,
            param_example: {id: '1001283596', name: 'test'}
        },
        saveMany: {
            url: 'Mongo/post/${name}/many',
            method: 'post',
            example: `{id: '1001283596'}`,
            param_example: {id: '1001283596', name: 'test'}
        },
        updateMany: {
            url: 'Mongo/post/${name}/many',
            method: 'put',
            example: `{condition: {id: '1001283596'}, update: {id: '1001283596'}}`,
            param_example: {condition: {id: '1001283596'}, update: {id: '1001283596', name: 'test'}}
        },
        delete: {
            url: 'Mongo/post/${name}/${id}',
            method: 'delete',
            param_example: {name: 'test', id: '1001283596'}
        },
        findOne: {
            url: '/Mongo/post/${name}/${id}',
            method: 'get',
            param_example: {
                name: 'test', id: '1001283596'
            }
        },
        find: {
            url: 'Mongo/post/${name}/${start}/${size}',
            method: 'post',
            param_example: {
                name: 'test', start: 0, size: 10,
                id: '1001283596'
            }
        },
        count: {
            url: 'Mongo/post/${name}/count',
            method: 'get',
            param_example: {name: 'test'}
        },
        countCondition: {
            url: 'Mongo/post/${name}/count',
            method: 'post',
            param_example: {name: 'test', id: '1001283596'}
        },
    }
};

let doBuildUrl = (apiCfg, params) => {
    let url = apiCfg.url.startsWith('http') ? apiCfg.url : appConfig.host + apiCfg.url;
    let pathParams = url.match(/\${\w+}/g);
    if (pathParams) {
        pathParams.forEach(param => {
            let paramKey = param.match(/\${(.*?)}/)[1];
            let paramValue = params[paramKey] || '';
            // 数组参数使用方式: 第一条数据作为路径参数, 第二条数据作为参数; 例如: /api/post/1001283596/many
            // 没有路径参数的话, 全部作为参数; 例如: /api/post/many
            if (Array.isArray(params) && params.length > 0) {
                let paramObj = params[0];
                paramValue = paramObj[paramKey] || '';
                delete paramObj[paramKey];
            }
            if (apiCfg.queryHash && 'queryHash' === paramKey && !paramValue) {
                paramValue = store.state.queryHash;
            }
            url = url.replace(param, paramValue);
            delete params[paramKey];
        })
    }
    return url;
};

/**
 * build for axios
 * @param apiCfg
 * @returns {(function(*=): *)|*}
 */
let build = (apiCfg) => {
    if (apiCfg) {
        let keys = Object.keys(apiCfg);
        if (keys.includes('url') && apiCfg.url) {
            let method = apiCfg.method || 'get';
            axios[method].toString();
            //try {
            //} catch (e) {
            //    throw new Error('错误的方法名! method: ' + method + ' url: ' + apiCfg.url);
            //}
            return (originParams = {}) => {
                let params = deepClone(originParams);
                let url = doBuildUrl(apiCfg, params);
                // 支持配置信息(如文件下载 blob)
                if (apiCfg.config) {
                    Object.assign(params, apiCfg.config)
                }
                // 支持表单提交(如文件上传 FormData)
                if (typeof (originParams) && originParams instanceof FormData) {
                    if (appConfig.isDebug) {
                        console.log('url', url, extractFormData(originParams), params, apiCfg);
                    }
                    return axios[method](url, originParams, params);
                }
                if (appConfig.isDebug) {
                    console.log('url', url, extractFormData(params), apiCfg);
                }
                // 数组参数使用方式: 第一条数据作为路径参数, 第二条数据作为参数; 例如: /api/post/1001283596/many
                // 没有路径参数的话, 全部作为参数; 例如: /api/post/many
                if (Array.isArray(params) && params.length > 0) {
                    params = params[1] ? params[1] : [];
                }
                return axios[method](url, params);
            }
        } else {
            keys.forEach(key => apiCfg[key] = build(apiCfg[key]));
            return apiCfg;
        }
    }
};

/**
 * build for GM_xmlhttpRequest
 * @param apiCfg
 * @returns {(function(*=): *)|*}
 */
let buildGm = (apiCfg) => {
    if (apiCfg) {
        let keys = Object.keys(apiCfg);
        if (keys.includes('url') && apiCfg.url) {
            let method = apiCfg.method || 'get';
            return (params = {}) => {
                params = deepClone(params);
                let url = doBuildUrl(apiCfg, params);
                // 数组参数使用方式: 第一条数据作为路径参数, 第二条数据作为参数; 例如: /api/post/1001283596/many
                // 没有路径参数的话, 全部作为参数; 例如: /api/post/many
                if (Array.isArray(params) && params.length > 0) {
                    params = params[1] ? params[1] : [];
                }
                let param = {
                    url,
                    method,
                    data: JSON.stringify(params),
                };
                console.log('url', url, params, apiCfg, param);
                window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
                return new Promise((resolve, reject) => {
                    param.onerror = (error) => {
                        reject(error)
                    };
                    param.onload = (resp) => {
                        let headers = {};
                        let headArr = resp.responseHeaders.split("\n");
                        headArr.forEach(head => {
                            let hArr = head.split(";");
                            let key = hArr.shift();
                            headers[key] = hArr.join().trim();
                        });
                        resp.headers = headers;
                        let data = {};
                        if (isJson(resp.responseText)) {
                            data = JSON.parse(resp.responseText);
                        }
                        resp.data = data;
                        resolve(resp);
                    };
                    if (!!GM_xmlhttpRequest) {
                        GM_xmlhttpRequest(param)
                    } else {
                        console.error("没有找到GM_xmlhttpRequest");
                    }
                });
            }
        } else {
            keys.forEach(key => apiCfg[key] = buildGm(apiCfg[key]));
            return apiCfg;
        }
    }
};

/**
 * usage in tamperMonkey:
 * let gmDownFile = buildGmDownFile();
 *
 * @returns {function(String): Promise<Object>}
 * @see <a href="https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest">tamperMonkey doc</a>
 * @author http://mmbro.gitee.com
 * @since 2022110
 */
let buildGmDownFile = () => {
    window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
    return (url) => {
        url = String(url).startsWith('http') ? url : appConfig.host + url;
        return new Promise((resolve, reject) => {
            let param = {
                url,
                method: 'get',
                responseType: 'blob'
            };
            param.onerror = (error) => {
                reject(error)
            };
            param.onload = (resp) => {
                resp.data = resp.response;
                resolve(resp);
            };
            if (!!GM_xmlhttpRequest) {
                GM_xmlhttpRequest(param)
            } else {
                console.error("没有找到GM_xmlhttpRequest");
            }
        })
    }
};

/**
 * usage in tamperMonkey:
 * let gmUpLoad = buildGmUpLoadFile();
 *
 * @returns {function(String, FormData): Promise<Object>}
 * @see <a href="https://www.tampermonkey.net/documentation.php#GM_xmlhttpRequest">tamperMonkey doc</a>
 * @author http://mmbro.gitee.com
 * @since 2022110
 */
let buildGmUpLoadFile = () => {
    window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
    return (url, formData) => {
        url = String(url).startsWith('http') ? url : appConfig.host + url;
        return new Promise((resolve, reject) => {
            let param = {
                url,
                data: formData,
                method: 'post',
                responseType: 'blob',
                overrideMimeType: 'multipart/form-data',
            };
            if (typeof (formData) === 'undefined') {
                reject('error! formData is not a FormData Object!')
            }
            param.onerror = (error) => {
                reject(error)
            };
            let isHeadersJson = (headers) => headers && headers['content-type'] && String(headers['content-type']).indexOf('application/json') >= 0;
            param.onload = (resp) => {
                let extractRespHeaders = (responseHeaders) => {
                    let headers = {};
                    let headArr = responseHeaders.split("\n");
                    headArr.forEach(head => {
                        if (head) {
                            let hArr = head.split(":");
                            let key = hArr.shift();
                            headers[key] = hArr.join().trim();
                        }
                    });
                    return headers;
                };
                resp.headers = extractRespHeaders(resp.responseHeaders);
                let data;
                if (isHeadersJson(headers)) {
                    if (isJson(resp.responseText)) {
                        data = JSON.parse(resp.responseText);
                    } else {
                        if (appConfig.isDebug) {
                            let contentType = headers['content-type'];
                            console.error('响应非json文本,contentType: %s', contentType);
                        }
                        data = resp.responseText;
                    }
                } else {
                    data = resp.responseText;
                }
                resp.data = data;
                resp.request = param;
                resolve(resp);
            };
            if (!!GM_xmlhttpRequest) {
                GM_xmlhttpRequest(param)
            } else {
                console.error("没有找到GM_xmlhttpRequest");
            }
        })
    }
};/**
 * Vue3 简单状态管理
 * @see <a href="https://v3.cn.vuejs.org/guide/state-management.html">从零打造简单状态管理</a>
 */
let store = {
    debug: true,
    state: Vue.reactive({
        runTask: null,
        apiDocs: null,
        lastMessageTime: new Date(),
        healthState: true,
        queryHash: '',
    }),

    setRunTask(newValue) {
        if (this.debug) {
            console.log('setRunTask triggered with', newValue)
        }

        this.state.runTask = newValue
    },

    clearRunTask(newValue) {
        if (this.debug) {
            console.log('clearRunTask triggered with', newValue)
        }

        let {runTask} = this.state;
        if (newValue && runTask && runTask.id === newValue.id) {
            this.state.runTask = null
        }
    },
};/**
 * axiosTamperMonkeyAdapter.js
 * axios GM_xmlhttpRequest adapter
 *
 * support axios version: axios@0.24.0
 * usage when loaded:
 * axios.defaults.adapter = xhrAdapter;
 * @param config
 * @returns {Promise<void>}
 * @author http://mmbro.gitee.com
 * @since 202217
 * @version 1.0.0
 */

var originAdapter = null;
if (!!axios && !!axios.defaults && !!axios.defaults.adapter) {
    originAdapter = axios.defaults.adapter;
}

// 配置依赖兼容隔离
if (typeof (appConfig) === 'undefined') {
    appConfig = {
        isDebug: true,
        axiosTimeOut: 60 * 1000,
    };
    if (typeof (unsafeWindow) !== 'undefined') {
        console.log('unsafeWindow is undefined, set up appConfig');
        unsafeWindow.appConfig = appConfig;
    }
}

// 配置依赖兼容隔离
if (typeof (isJson) === 'undefined') {
    /**
     * 判断字符串是否为json(xhrAdapter依赖)
     * @param str
     * @returns {boolean}
     * @see <a href="https://www.cnblogs.com/lanleiming/p/7096973.html">【最简单的方法】js判断字符串是否为JSON格式（20180115更新）</a>
     */
    window.isJson = (str) => {
        if (typeof str === 'string') {
            try {
                let obj = JSON.parse(str);
                return !!(typeof obj === 'object' && obj);
            } catch (e) {
                console.log('isJson error： %o !!!', str, e);
            }
        }
        return false;
    };
}

function xhrAdapter(config) {
    // before keep no change for axios origin code.
    return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data;
        var requestHeaders = config.headers;
        var responseType = config.responseType;
        var overrideMimeType = config.overrideMimeType;
        var onCanceled;

        // 全局超时设置
        if (!config.timeout) {
            config.timeout = appConfig.axiosTimeOut;
            if (appConfig.isDebug) {
                console.debug(`超时全局配置=${config.timeout}`);
            }
        }
        // var request = new XMLHttpRequest();
        //
        // // HTTP basic authentication
        // if (config.auth) {
        //     var username = config.auth.username || '';
        //     var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
        //     requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
        // }
        //
        // var fullPath = buildFullPath(config.baseURL, config.url);
        // request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
        //
        // // Set the request timeout in MS
        // request.timeout = config.timeout;
        //
        // function onloadend() {
        //     if (!request) {
        //         return;
        //     }
        //     // Prepare the response
        //     var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
        //     var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
        //         request.responseText : request.response;
        //     var response = {
        //         data: responseData,
        //         status: request.status,
        //         statusText: request.statusText,
        //         headers: responseHeaders,
        //         config: config,
        //         request: request
        //     };
        //
        //     settle(function _resolve(value) {
        //         resolve(value);
        //         done();
        //     }, function _reject(err) {
        //         reject(err);
        //         done();
        //     }, response);
        //
        //     // Clean up request
        //     request = null;
        // }
        //
        // if ('onloadend' in request) {
        //     // Use onloadend if available
        //     request.onloadend = onloadend;
        // } else {
        //     // Listen for ready state to emulate onloadend
        //     request.onreadystatechange = function handleLoad() {
        //         if (!request || request.readyState !== 4) {
        //             return;
        //         }
        //
        //         // The request errored out and we didn't get a response, this will be
        //         // handled by onerror instead
        //         // With one exception: request that using file: protocol, most browsers
        //         // will return status as 0 even though it's a successful request
        //         if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        //             return;
        //         }
        //         // readystate handler is calling before onerror or ontimeout handlers,
        //         // so we should call onloadend on the next 'tick'
        //         setTimeout(onloadend);
        //     };
        // }
        //
        // // Handle browser request cancellation (as opposed to a manual cancellation)
        // request.onabort = function handleAbort() {
        //     if (!request) {
        //         return;
        //     }
        //
        //     reject(createError('Request aborted', config, 'ECONNABORTED', request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle low level network errors
        // request.onerror = function handleError() {
        //     // Real errors are hidden from us by the browser
        //     // onerror should only fire if it's a network error
        //     reject(createError('Network Error', config, null, request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle timeout
        // request.ontimeout = function handleTimeout() {
        //     var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
        //     var transitional = config.transitional || defaults.transitional;
        //     if (config.timeoutErrorMessage) {
        //         timeoutErrorMessage = config.timeoutErrorMessage;
        //     }
        //     reject(createError(
        //         timeoutErrorMessage,
        //         config,
        //         transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        //         request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Add xsrf header
        // // This is only done if running in a standard browser environment.
        // // Specifically not if we're in a web worker, or react-native.
        // if (utils.isStandardBrowserEnv()) {
        //     // Add xsrf header
        //     var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        //         cookies.read(config.xsrfCookieName) :
        //         undefined;
        //
        //     if (xsrfValue) {
        //         requestHeaders[config.xsrfHeaderName] = xsrfValue;
        //     }
        // }
        //
        // // Add headers to the request
        // if ('setRequestHeader' in request) {
        //     utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        //         if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
        //             // Remove Content-Type if data is undefined
        //             delete requestHeaders[key];
        //         } else {
        //             // Otherwise add header to the request
        //             request.setRequestHeader(key, val);
        //         }
        //     });
        // }
        //
        // // Add withCredentials to request if needed
        // if (!utils.isUndefined(config.withCredentials)) {
        //     request.withCredentials = !!config.withCredentials;
        // }
        //
        // // Add responseType to request if needed
        // if (responseType && responseType !== 'json') {
        //     request.responseType = config.responseType;
        // }
        //
        // // Handle progress if needed
        // if (typeof config.onDownloadProgress === 'function') {
        //     request.addEventListener('progress', config.onDownloadProgress);
        // }
        //
        // // Not all browsers support upload events
        // if (typeof config.onUploadProgress === 'function' && request.upload) {
        //     request.upload.addEventListener('progress', config.onUploadProgress);
        // }
        //
        // if ('onloadend' in request) {
        //     // Use onloadend if available
        //     request.onloadend = onloadend;
        // } else {
        //     // Listen for ready state to emulate onloadend
        //     request.onreadystatechange = function handleLoad() {
        //         if (!request || request.readyState !== 4) {
        //             return;
        //         }
        //
        //         // The request errored out and we didn't get a response, this will be
        //         // handled by onerror instead
        //         // With one exception: request that using file: protocol, most browsers
        //         // will return status as 0 even though it's a successful request
        //         if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        //             return;
        //         }
        //         // readystate handler is calling before onerror or ontimeout handlers,
        //         // so we should call onloadend on the next 'tick'
        //         setTimeout(onloadend);
        //     };
        // }
        //
        // // Handle browser request cancellation (as opposed to a manual cancellation)
        // request.onabort = function handleAbort() {
        //     if (!request) {
        //         return;
        //     }
        //
        //     reject(createError('Request aborted', config, 'ECONNABORTED', request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle low level network errors
        // request.onerror = function handleError() {
        //     // Real errors are hidden from us by the browser
        //     // onerror should only fire if it's a network error
        //     reject(createError('Network Error', config, null, request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Handle timeout
        // request.ontimeout = function handleTimeout() {
        //     var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
        //     var transitional = config.transitional || defaults.transitional;
        //     if (config.timeoutErrorMessage) {
        //         timeoutErrorMessage = config.timeoutErrorMessage;
        //     }
        //     reject(createError(
        //         timeoutErrorMessage,
        //         config,
        //         transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        //         request));
        //
        //     // Clean up request
        //     request = null;
        // };
        //
        // // Add xsrf header
        // // This is only done if running in a standard browser environment.
        // // Specifically not if we're in a web worker, or react-native.
        // if (utils.isStandardBrowserEnv()) {
        //     // Add xsrf header
        //     var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        //         cookies.read(config.xsrfCookieName) :
        //         undefined;
        //
        //     if (xsrfValue) {
        //         requestHeaders[config.xsrfHeaderName] = xsrfValue;
        //     }
        // }
        //
        // // Add headers to the request
        // if ('setRequestHeader' in request) {
        //     utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        //         if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
        //             // Remove Content-Type if data is undefined
        //             delete requestHeaders[key];
        //         } else {
        //             // Otherwise add header to the request
        //             request.setRequestHeader(key, val);
        //         }
        //     });
        // }
        //
        // // Add withCredentials to request if needed
        // if (!utils.isUndefined(config.withCredentials)) {
        //     request.withCredentials = !!config.withCredentials;
        // }
        //
        // // Add responseType to request if needed
        // if (responseType && responseType !== 'json') {
        //     request.responseType = config.responseType;
        // }
        //
        // // Handle progress if needed
        // if (typeof config.onDownloadProgress === 'function') {
        //     request.addEventListener('progress', config.onDownloadProgress);
        // }
        //
        // // Not all browsers support upload events
        // if (typeof config.onUploadProgress === 'function' && request.upload) {
        //     request.upload.addEventListener('progress', config.onUploadProgress);
        // }
        //
        // if (config.cancelToken || config.signal) {
        //     // Handle cancellation
        //     // eslint-disable-next-line func-names
        //     onCanceled = function (cancel) {
        //         if (!request) {
        //             return;
        //         }
        //         reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
        //         request.abort();
        //         request = null;
        //     };
        //
        //     config.cancelToken && config.cancelToken.subscribe(onCanceled);
        //     if (config.signal) {
        //         config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
        //     }
        // }
        //
        // if (!requestData) {
        //     requestData = null;
        // }

        let isHeadersJson = (headers) => headers && headers['content-type'] && String(headers['content-type']).indexOf('application/json') >= 0;

        if (typeof (GM_xmlhttpRequest) === 'undefined') {
            // use axios original code with noChange.
            // 使用axios原来方式调用
            // Send the request
            if (typeof (appConfig) !== 'undefined' && appConfig.isDebug) {
                console.log('use origin axios adapter; config: ', config);
            }
            originAdapter(config).then(suc => resolve(suc)).catch(err => reject(err));
            // resolve(requestData);
        } else {
            // after now, use GM_xmlhttpRequest to adapter axios request
            // 使用GM_xmlhttpRequest方式调用
            if (typeof (appConfig) !== 'undefined' && appConfig.isDebug) {
                console.log('use new axios adapter; config: ', config);
            }
            window.GM_xmlhttpRequest = typeof (GM_xmlhttpRequest) === 'undefined' ? undefined : GM_xmlhttpRequest;
            let url = config.url;
            let method = config.method.toUpperCase();
            let data = requestData;
            let headers = config.headers;
            if (!data) {
                data = null;
            } else {
                if (isHeadersJson(headers)) {
                    if (isJson(data)) {
                        data = JSON.parse(data);
                    }
                } else {
                    if (typeof (data) === 'undefined') {
                        data = null;
                    } else if (typeof (data) !== 'string') {
                        // 添加FormData支持
                        if (data instanceof FormData) {
                            // @see <a href="https://blog.csdn.net/weixin_34413802/article/details/88722992">前端通过axios和FormData实现文件上传功能遇到的坑</a>
                            // 让浏览器来继续设置这个值
                            delete headers['content-type'];
                        } else {
                            data = JSON.stringify(data);
                        }
                    }
                }
            }
            let param = {
                url,
                method,
                headers,
                data,
                timeout: config.timeout,
            };
            if (responseType) {
                // 支持blob下载
                param.responseType = responseType;
            }
            if (overrideMimeType) {
                // 支持form表单提交
                if (appConfig.isDebug) {
                    console.log('支持form表单提交');
                }
                param.overrideMimeType = overrideMimeType;
            }
            param.onerror = (error) => {
                reject(error)
            };
            param.ontimeout = (timeout) => {
                console.error('execute timeout! config=%o, timeout=%o', config, timeout);
                reject(timeout);
            };
            param.onload = (resp) => {
                let headers = {};
                let headArr = resp.responseHeaders.split("\n");
                headArr.forEach(head => {
                    if (head) {
                        let hArr = head.split(":");
                        let key = hArr.shift();
                        headers[key] = hArr.join().trim();
                    }
                });
                resp.headers = headers;
                let data;
                if (isHeadersJson(headers)) {
                    if (isJson(resp.responseText)) {
                        data = JSON.parse(resp.responseText);
                    } else {
                        if (typeof (appConfig) !== 'undefined' && appConfig.isDebug) {
                            let contentType = headers['content-type'];
                            console.error('响应非json文本,contentType: %s', contentType);
                        }
                        data = resp.responseText;
                    }
                } else {
                    // 支持 blob, json 请求
                    if (responseType && (responseType === 'blob' || responseType === 'json')) {
                        data = resp.response;
                    } else {
                        data = resp.responseText;
                    }
                }
                resp.data = data;
                resp.config = config;
                resp.request = param;

                // 参考 axios ["data", "status", "statusText", "headers", "config", "request"]
                // // Prepare the response
                // var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
                // var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
                //     request.responseText : request.response;
                // var response = {
                //     data: responseData,
                //     status: request.status,
                //     statusText: request.statusText,
                //     headers: responseHeaders,
                //     config: config,
                //     request: request
                // };
                resolve(resp);
            };
            if (!!GM_xmlhttpRequest) {
                GM_xmlhttpRequest(param)
            } else {
                console.error("没有找到GM_xmlhttpRequest");
            }
        }
    });
};
var template = `
<div class="list">
    <table v-if="list.length > 0 && Object.keys(showModel).length > 0" border="0" cellspacing="0" cellpadding="5">
        <tr><th v-for="key in Object.keys(showModel)">{{doGetKey(key)}}</th><th v-if="controlSlot">操作</th></tr>
        <tr :class="{green: doSuccess(item)}" v-for="item in list">
            <td v-for="key in Object.keys(showModel)">{{doFormat(item,key)}}</td>
            <td v-if="controlSlot"><slot :item="item"></slot></td>
            
        </tr>
    </table>
    <div class="no-data" v-else>没有数据</div>
</div>
`;

let List = {
    name: 'List',
    components: {},
    template,
    props: {
        list: {
            type: Array,
            default: []
        },
        showModel: {
            type: Object,
            default: {}
        },
        schema: {
            type: String,
            default: ''
        },
    },
    methods: {
        doFormat(item, key) {
            if (this.showModel[key] && this.showModel[key].formatter) {
                if ('date' === this.showModel[key].type) {
                    return new Date(item[key]).fmt(this.showModel[key].formatter);
                } else {
                    return item[key];
                }
            } else if ('text' === this.showModel[key].type) {
                if (this.showModel[key].maxLength && this.showModel[key].maxLength > 0) {
                    let maxLength = this.showModel[key].maxLength;
                    if (typeof (item[key]) === 'string' && item[key].length > maxLength) {
                        return item[key].substr(0, maxLength) + '...';
                    } else {
                        return item[key];
                    }
                } else {
                    return item[key];
                }
            } else {
                return item[key];
            }
        },
        doSuccess(item) {
            let successKeys = Object.keys(this.showModel).filter(key => !!this.showModel[key] && this.showModel[key].hasOwnProperty('success'));
            let success = successKeys.length > 0;
            successKeys.forEach(key => {
                if (item[key] !== this.showModel[key]['success']) {
                    success = false;
                }
            });
            return success;
        },
        doGetKey(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
    },
    data() {
        const {useSlots} = Vue;
        return {
            sharedState: store.state,
            controlSlot: !!useSlots().default
        }
    }
};
var template = `
<div class="form">
    <div class="prop" v-if="Object.keys(data).length > 0 && Object.keys(showModel).length > 0">
        <div v-for="key in Object.keys(showModel)">
            <div class="label"><label>{{doGetKey(key)}}:</label></div>
            <div class="input">
                <div v-if="showModel[key] && 'boolean' === showModel[key].type">
                    <input type="radio" value="true" v-model="data[key]">
                    <label>true</label>
                    <input type="radio" value="false" v-model="data[key]">
                    <label>false</label>
                </div>
                <div v-else-if="showModel[key] && 'map' === showModel[key].type">
                    <div class="map" v-for="(mValue,mKey,index) in data[key]">
                        <input class="key" type="text" :value="mKey" @input="changeKey(key,index,mKey,$event.target.value)">
                        <label> : </label>
                        <input class="value" type="text" :value="mValue" @input="changeValue(key,index,mValue,$event.target.value)">
                        <button class="del" @click="changeRemove(key,index)">-</button>
                    </div>
                    <button class="add" @click="changeAdd(key)">+</button>
                </div>
                <div v-else-if="showModel[key] && 'textarea' === showModel[key].type">
                    <div class='textarea'>
                        <textarea wrap="off" v-model="data[key]"></textarea>
                    </div>
                </div>
                <input v-else type="text" v-model="data[key]"/>
            </div>
            <div style="clear:both;"></div>
        </div>
        <div style="clear:both;"></div>
    </div>
    <div class="no-data" v-else>没有模型</div>
    <div class="buttons">
        <button @click="submit">提交</button>
        <button @click="cancel">取消</button>
        <div style="clear:both;"></div>
    </div>

</div>
`;


let Form = {
    name: 'Form',
    components: {},
    template,
    props: {
        data: {
            type: Object,
            default: {}
        },
        edit: {
            type: Object,
            default: {}
        },
        showModel: {
            type: Object,
            default: {}
        },
        schema: {
            type: String,
            default: ''
        },
    },
    methods: {
        submit() {
            this.$emit('submit')
        },
        cancel() {
            this.$emit('cancel')
        },
        changeKey(key, index, oldVal, newVal) {
            let map = this.data[key];
            let listElement = Object.keys(map).map(k => [k, map[k]]);
            listElement[index][0] = newVal;
            let newObj = {};
            listElement.forEach(arr => newObj[arr[0]] = arr[1]);
            this.data[key] = newObj;
        },
        changeValue(key, index, oldVal, newVal) {
            let map = this.data[key];
            let listElement = Object.keys(map).map(k => [k, map[k]]);
            listElement[index][1] = newVal;
            let newObj = {};
            listElement.forEach(arr => newObj[arr[0]] = arr[1]);
            this.data[key] = newObj;
        },
        changeRemove(key, index) {
            console.log('changeRemove');
            let map = this.data[key];
            let listElement = Object.keys(map).map(k => [k, map[k]]);
            let newObj = {};
            listElement.forEach((arr, ind) => {
                if (index !== ind) {
                    newObj[arr[0]] = arr[1];
                }
            });
            this.data[key] = newObj;
        },
        changeAdd(key) {
            let map = this.data[key];
            map[''] = '';
        },
        doGetKey(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
    },
    data() {
        return {
            sharedState: store.state,
        }
    },
};
var template = `
<div class="runner">
    <hr/>
    <div v-if="banMsg">{{banMsg}}</div>
    <div v-if="sharedState.runTask">
        runTask
        <div v-if="runTask" class="form">
            <div v-for="key in Object.keys(showModel)">
                <div class="label"><label>{{doGetKey(key)}}:</label></div>
                <div class="input">{{doFormat(runTask,key)}}</div>
                <div style="clear:both;"></div>
            </div>
        </div>
    </div>
    <div class="no-data" v-else>没有数据</div>
</div>
`;

let Runner = {
    name: 'Runner',
    components: {},
    template,
    props: {},
    data() {
        return {
            sharedState: store.state,
            punishMs: appConfig.heartBeatPunish * 1000,
            lastTimeOutId: -1,
            timeoutMs: appConfig.heartBeatFixedDelay * 1000,
            lastSuccessTime: Date.now(),
            lastCostTime: -1,
            exit: false,
            sleepTimeMs: 10,
            runTask: null,
            nextList: [],
            nextModel: {
                // 下一个方法名
                name: '',
                // 下一个方法入参
                param: {}
            },
            // 任务栈计数
            nextCount: 0,
            // 任务栈空闲计数 空闲10次
            nextNothingCount: 0,
            runnerId: UUID(),
            config: {},
            isUserCheck: false,
            isUserOk: false,
            isProxyOk: false,
            // 任务是否正常
            isTaskOk: false,
            taskFailCount: 0,
            taskCheckUrl: '',
            sharedData: null,
            username: '',
            current: -1,
            currentId: '',
            total: -1,
            tempPost: null,
            tempPostEdges: null,
            tempPostEdgesSize: null,
            tempPostEdgesIndex: 0,
            tempPostEdgesFailCount: 0,
            tempPicImagePaths: [],
            tempPicList: [],
            ignoreGetImage: appConfig.ignoreGetImage,
            updateEdgeList: [],
            updateEdgeListIndex: -1,
            updateEdgeListSize: -1,
            schema: '任务信息',
            showModel: {
                taskName: '',
                currentId: '',
                // threadSize:'',
                // first:'',
                total: '',
                current: '',
                percentSuccess: '',
                // timeoutExecTime:'',
                // runningThread:'',
                running: '',
                paused: '',
                firstExecTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                lastExecTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                latestActiveTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                createdTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            banMsg: '',
        }

    },
    methods: {
        doGetKey(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
        doFormat(item, key) {
            if (this.showModel[key] && this.showModel[key].formatter) {
                if ('date' === this.showModel[key].type) {
                    return new Date(item[key]).fmt(this.showModel[key].formatter);
                } else {
                    return item[key];
                }
            } else if ('text' === this.showModel[key].type) {
                if (this.showModel[key].maxLength && this.showModel[key].maxLength > 0) {
                    let maxLength = this.showModel[key].maxLength;
                    if (typeof (item[key]) === 'string' && item[key].length > maxLength) {
                        return item[key].substr(0, maxLength) + '...';
                    } else {
                        return item[key];
                    }
                } else {
                    return item[key];
                }
            } else {
                return item[key];
            }
        },
        async doInit() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                return;
            }
            this.runTask = runTask;
            let {firstExecTime = 0} = runTask;
            if (firstExecTime <= 0) {
                firstExecTime = Date.now();
                runTask.firstExecTime = firstExecTime;
            }
            runTask.lastExecTime = Date.now();
            let config = await this.doGetConfig();
            let total = await this.doGetTotal(runTask.total);
            this.total = total;
            runTask.total = total;
            this.config = config;
            let {isUserCheck = false} = config;
            let {sleepTaskMs} = config;
            sleepTaskMs = sleepTaskMs > 0 ? sleepTaskMs : appConfig.defaultSleepTaskMs;
            let lastTaskMs = localStorage.getItem("lastTaskMs");
            if (lastTaskMs && Date.now() - Number(lastTaskMs) < sleepTaskMs) {
                let msg = ['任务启动失败!', `上次任务过去时间不够久 ${new Date(Number(lastTaskMs)).fmt()}`];
                console.warn(...msg);
                this.$message({message: msg});
                this.doStop();
                return;
            }
            this.isUserCheck = isUserCheck;
            if (!isUserCheck) {
                this.isUserOk = true;
            }
            let {sleepTimeMs = 500} = config;
            this.sleepTimeMs = sleepTimeMs;
            this.timeoutMs = sleepTimeMs;
            this.doPushNext({name: 'update'});
            this.doStart();
        },
        async doStart() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                let msg = ['任务启动异常!', 'runTask:' + typeof (runTask)];
                console.error(...msg);
                this.$message({message: msg, level: 'error'});
                this.doStop();
                return;
            }
            let {taskName = ''} = runTask;
            this.$message({message: ['任务开始', taskName]});
            let startCount = 0;
            while (!this.exit) {
                if (this.sleepTimeMs <= 1000) {
                    this.sleepTimeMs = 1000;
                }
                await this.sleep(this.sleepTimeMs);
                // ------
                // ------
                // ------ 局部函数
                let isPaused = async () => {
                    let start = Date.now();
                    while (runTask.paused && runTask.running) {
                        await this.sleep(this.sleepTimeMs);
                        let past = Date.now() - start;
                        if (past / 1000 > 30) {
                            let msg = [`任务暂停中 ${this.runnerId}`, `current=${this.current} cid=${this.currentId}`];
                            console.log(...msg);
                            this.$message({message: msg});
                            start = Date.now();
                        }
                    }
                };
                let isFinished = () => this.current + 1 === this.total;
                let isRunning = () => runTask.running && !appConfig.allStop;
                let isTaskInterval = () => 0;
                // ------
                // ------
                // ========
                // ========
                // ======== 任务执行
                // ========
                // ========
                // ========
                // ========
                if (isFinished()) {
                    localStorage.setItem("lastTaskMs", String(Date.now()));
                    this.$message();
                    this.doStop();
                    this.exit = true;
                    break;
                }
                // =====  检查用户
                if (this.isUserCheck) {
                    /*
                    检查用户登录
                     */
                }
                // ====== 获取登录信息
                await isPaused();
                try {
                    runTask.latestActiveTime = Date.now();
                    await this.doGetSharedData();
                } catch (e) {
                    let msg = ['获取登录信息失败!'];
                    this.$message({message: msg, level: 'error'});
                    console.log(...msg, e);
                    this.exit = true;
                    this.doStop();
                    continue;
                }
                // ========= 检查登录信息
                await isPaused();
                try {
                    runTask.latestActiveTime = Date.now();
                    await this.doCheckTask();
                } catch (e) {
                    let msg = ['检查任务失败!', e];
                    console.log(...msg);
                    this.$message({message: msg, level: 'error'});
                    this.exit = true;
                    this.doStop();
                    continue;
                }
                // ==========  执行任务
                if (!runTask.running) {
                    this.doStop();
                    continue;
                }
                if (runTask.paused) {
                    continue;
                }
                /*
                获取用户id
                 */
                await isPaused();
                try {
                    if (this.current < 0) {
                        runTask.latestActiveTime = Date.now();
                        await this.doGetNextId();
                    }
                } catch (e) {
                    console.log('获取用户id失败', e);
                    await this.doPunish(this.punishMs);
                    continue;
                }
                /*
                获取用户帖子
                 */
                await isPaused();
                try {
                    runTask.latestActiveTime = Date.now();
                    await this.doGetPost();
                } catch (e) {
                    console.log('获取用户帖子失败! uid=' + this.currentId, e);
                    await this.doPunish(this.punishMs);
                    continue;
                }
                /*
                获取用户帖子图片及更新
                 */
                while (this.current !== -1) {
                    await this.sleep(this.sleepTimeMs);
                    await isPaused();
                    if (!isRunning()) {
                        break;
                    }
                    try {
                        runTask.latestActiveTime = Date.now();
                        await this.doGetPic();
                    } catch (e) {
                        console.log('下载图片失败! uid=' + this.currentId, e);
                        await this.doPunish(this.punishMs);
                    }
                }
                /*
                上传未传完的用户帖子数据
                 */
                this.tempPostEdgesFailCount = 0;
                this.updateEdgeListIndex = 0;
                this.updateEdgeListSize = this.updateEdgeList.length;
                while (this.updateEdgeList.length > 0) {
                    await this.sleep(this.sleepTimeMs);
                    await isPaused();
                    if (!isRunning()) {
                        break;
                    }
                    try {
                        runTask.latestActiveTime = Date.now();
                        await this.doUpdateEdge();
                    } catch (e) {
                        console.log('上传用户帖子失败! uid=' + this.currentId, e);
                        await this.doPunish(this.punishMs);
                    }
                }

                if (!isRunning()) {
                    console.log('一次循环执行完毕', startCount++);
                    break;
                }
                this.update();
            }
            this.doStop();
            console.log('任务退出!')
        },
        async doPunish(timeMs = 0) {
            this.timeoutMs = this.timeoutMs + this.punishMs;
            await this.sleep(this.timeoutMs);
            this.timeoutMs = 0;
        },
        update() {
            let {runTask} = this.sharedState;
            if (runTask) {
                console.log('更新任务');
                this.$emit('update', runTask);
            }
        },
        doGetTotal(failNumber = 0) {
            return new Promise((resolve, reject) => {
                api.ins.count().then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    resolve(failNumber);
                }).catch(e => {
                    console.error("读取用户总数失败!", e);
                    resolve(failNumber);
                })
            })
        },
        doGetConfig() {
            return new Promise(((resolve, reject) => {
                api.config.getAll().then(res => {
                    if (res.data && res.data.code === 0) {
                        if (res.data.data.length > 0) {
                            let config = res.data.data[0];
                            resolve(config);
                            return;
                        }
                    }
                    resolve({});
                }).catch((e) => {
                    console.error("读取配置文件失败!", e);
                    resolve({});
                })
            }))
        },
        async sleep(sleepMs = this.sleepTimeMs) {
            return new Promise(((resolve, reject) => {
                setTimeout(() => {
                    resolve()
                }, sleepMs)
            }))
        },
        doPushNext(param = null) {
            /*
            这里可以加前后置执行器, 所有任务栈的添加都用这个方法
             */
            if (param) {
                // @Deprecated
                // this.nextList.push(param);
            }
        },
        async doNext() {
            this.nextCount++;
            let stackNext = this.doNext;
            let next = this.nextList.shift();
            let {sleepTimeMs = 100} = this;
            this.lastTimeOutId = setTimeout(() => {
                clearTimeout(this.lastTimeOutId);
                if (this.exit) {
                    console.log('任务栈退出', this.runnerId);
                    return;
                }
                if (!next) {
                    this.timeoutMs = this.punishMs;
                    this.nextNothingCount++;
                    if (this.nextNothingCount > 10) {
                        let nothingSecond = this.nextNothingCount * this.punishMs / 1000;
                        let msg = ['任务异常', '任务栈长期空闲' + nothingSecond + 's'];
                        console.log(...msg);
                        this.$message({message: msg, level: 'error'})
                    }
                    console.warn('任务栈空闲惩罚: %ss', this.timeoutMs / 1000);
                    this.doWhatToDo();
                    stackNext();
                } else {
                    let funName = next.name;
                    let param = next.param;
                    let nextCall = this[funName];
                    if (!nextCall) {
                        this.timeoutMs = this.punishMs;
                        console.warn('任务栈无效参数惩罚: %ss next: %o', this.timeoutMs / 1000, next);
                        stackNext();
                        return;
                    }
                    let promise;
                    if (typeof (param) === 'undefined') {
                        promise = nextCall();
                    } else {
                        promise = nextCall(param);
                    }
                    if (typeof (promise) === 'object' && promise instanceof Promise) {
                        promise.then(() => {
                            this.timeoutMs = this.sleepTimeMs;
                            stackNext();
                        }).catch(e => {
                            this.timeoutMs = this.timeoutMs + this.punishMs;
                            console.warn('任务栈调用异常惩罚: %ss', this.timeoutMs / 1000);
                            console.error('任务栈调用异常! funName: %s param: %o', funName, param, e);
                            e = typeof (e) === 'undefined' ? 'undefined' : e;
                            let err = e instanceof Error ? e.message : JSON.stringify(e);
                            let msg = ['任务栈调用异常!', 'funName: ' + funName, 'param: ' + JSON.stringify(param), '错误信息: ' + err];
                            this.$message({message: msg, level: 'error', duration: 5});
                            stackNext();
                        })
                    } else {
                        this.timeoutMs = this.sleepTimeMs;
                        stackNext();
                    }
                }
            }, this.timeoutMs)
        },
        doCheckLogin() {

        },
        doStop() {
            let {runTask} = this.sharedState;
            if (runTask) {
                runTask.paused = false;
                runTask.running = false;
                this.update();
                this.sharedState.runTask = null;
                this.exit = true;
                let {taskName = ''} = runTask;
                this.$message({message: ['任务结束', taskName]});
            }
        },
        doWhatToDo() {
            let {runTask} = this.sharedState;
            if (runTask) {
                if (this.nextList.length > 0) {
                    return;
                }
                if (this.isUserCheck) {
                    // 添加检查用户任务栈
                }
                if (this.isUserOk) {
                    // 添加任务下载任务栈
                    if (this.isTaskOk) {
                        if (runTask.running && !runTask.paused) {
                            let next_getNextId = {
                                name: 'doGetNextId'
                            };
                            let next_getPost = {
                                name: 'doGetPost'
                            };
                            let next_getPic = {
                                name: 'doGetPic'
                            };
                            let next_updateEdge = {
                                name: 'doUpdateEdge'
                            };
                            this.nextList.push(next_getNextId);
                            this.nextList.push(next_getPost);
                            this.nextList.push(next_getPic);
                            this.nextList.push(next_updateEdge);
                        }
                    } else {
                        // 检查任务状态
                        if (!this.sharedData) {
                            let next_getSharedData = {
                                name: 'doGetSharedData'
                            };
                            this.nextList.push(next_getSharedData);
                        }
                        let next_checkTask = {
                            name: 'doCheckTask'
                        };
                        this.nextList.push(next_checkTask);
                    }
                }
            }
        },
        async doUpdateEdge() {
            return new Promise((resolve, reject) => {
                let updateEdgeFailMax = 10;
                if (this.tempPostEdgesFailCount >= updateEdgeFailMax) {
                    this.tempPostEdgesFailCount = 0;
                    let failSize = this.updateEdgeList.length;
                    let {current: cur, currentId: cid} = this;
                    let msg = ['放弃更新帖子数据',
                        '连续' + updateEdgeFailMax + '次更新失败',
                        '放弃[' + failSize + ']条未更新数据',
                        'cur= ' + cur + ' cid= ' + cid];
                    console.log(...msg);
                    this.$message({message: msg, level: 'error'});
                    this.updateEdgeList.clear();
                    resolve();
                    return;
                }
                let edge = this.updateEdgeList.shift();
                if (!edge) {
                    resolve();
                    return;
                }
                api.post.addOrUpdate(edge).then(res => {
                    let {current, currentId: cid, tempPostEdgesIndex: index} = this;
                    if (res.data && res.data.code === 0) {
                        this.updateEdgeListIndex++;
                        let msg = ['更新帖子成功!', `${this.updateEdgeListIndex} of ${this.updateEdgeListSize}`, 'cid=' + cid, 'id=' + edge.id];
                        console.log(...msg);
                        this.$message({message: msg});
                        resolve();
                        return;
                    }
                    let msg = ['更新帖子失败!', `${this.updateEdgeListIndex} of ${this.updateEdgeListSize}`, 'cid=' + cid, 'id=' + edge.id];
                    console.log(msg, res);
                    this.$message({message: msg, level: 'error'});
                    this.tempPostEdgesFailCount++;
                    this.updateEdgeList.push(edge);
                    // 此处跳转到最外层主循环,不会进到下面行的catch
                    reject(res);
                }).catch(e => {
                    let msg = ['更新帖子失败!!', `${this.updateEdgeListIndex} of ${this.updateEdgeListSize}`, 'msg=' + e.message, 'id=' + edge.id];
                    console.log(msg, e);
                    this.$message({message: msg, level: 'error'});
                    this.tempPostEdgesFailCount++;
                    this.updateEdgeList.push(edge);
                    reject(e);
                })
            })
        },
        async doGetPic() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                let msg = ['获取getPic异常!', 'runTask:' + typeof (runTask)];
                console.error(...msg);
                this.$message({message: msg, level: 'error'});
                return;
            }
            let tempPost = this.tempPost;
            if (!tempPost) {
                let msg = ['获取getPic异常!', 'tempPost:' + typeof (tempPost)];
                console.error(...msg);
                this.$message({message: msg, level: 'error'});
                return;
            }
            if (this.tempPostEdgesIndex < this.tempPostEdgesSize) {
                // a edge post
                let nextEdge = this.tempPostEdges[this.tempPostEdgesIndex].node;
                let likeCount = nextEdge['edge_media_preview_like']['count'];
                let commentCount = nextEdge['edge_media_to_comment']['count'];
                nextEdge.timingSequenceData = [
                    {
                        "likeCount": likeCount,
                        "commentCount": commentCount,
                        "currentTime": new Date()
                    }
                ];
                let image_paths = [];
                nextEdge['image_paths'] = image_paths;
                let displayUrl = nextEdge['display_url'];
                if (!displayUrl) {
                    let msg = ['没有图片', ' current: ' + this.current + ' uid: ' + this.currentId, ' pid: ' + this.currentId + ' i: ' + this.tempPostEdgesIndex];
                    console.warn(...msg);
                    this.$message({message: msg});
                    this.tempPostEdgesIndex++;
                    this.tempPostEdgesFailCount = 0;
                    return;
                }
                let failMethod = (e) => {
                    if (this.tempPostEdgesFailCount >= 3) {
                        let msg = ['图片下载失败!', '图片3次下载失败', '跳过当前图片下载 index: ' + this.tempPostEdgesIndex];
                        console.log(...msg, e);
                        this.$message({message: msg, level: 'error'});
                        this.tempPostEdgesIndex++;
                        this.tempPostEdgesFailCount = 0;
                        if (this.ignoreGetImage) {
                            this.updateEdgeList.push(nextEdge);
                        }
                        throw new Error('图片下载失败,超时');
                    }
                    this.tempPostEdgesFailCount++;
                };
                let typeName = nextEdge['__typename']
                let imageUrls = [displayUrl]
                if ('GraphSidecar' === typeName) {
                    let {edge_sidecar_to_children = {}} = nextEdge
                    let {edges = []} = edge_sidecar_to_children
                    let displayUrls = edges.map(e => e.node.display_url)
                    imageUrls.push(...displayUrls)
                    // 图片url去重
                    imageUrls = Array.from(new Set(imageUrls))
                    console.log('==== 多图片 ====', imageUrls, imageUrls.map(e => sha1(e)))
                }
                /*
                下载帖子图片;
                并且上传帖子图片
                 */
                for (let i = 0; i < imageUrls.length; i++) {
                    let imageUrl = imageUrls[i]
                    // todo 这里要拆分成单独的图片下载上传
                    try {
                        let imagePath = await this.doDownLoadPicture(imageUrl, i + 1, imageUrls.length)
                        if (imagePath) {
                            image_paths.push(imagePath)
                        } else {
                            failMethod(e)
                            return
                        }
                    } catch (e) {
                        failMethod(e)
                        return
                    }
                }
                nextEdge['image_paths'] = image_paths;
                this.tempPostEdgesIndex++;
                this.updateEdgeList.push(nextEdge);

            } else {
                // 当前 post 下载完成
                let msg = ['帖子 ' + this.currentId + '下载完成!'];
                console.log(...msg);
                this.tempPostEdges = null;
                this.tempPostEdgesIndex = 0;
                this.tempPostEdgesSize = 0;
                this.tempPostEdgesFailCount = 0;
                this.tempPicImagePaths = [];
                // 执行下一个id
                this.current = -1;
                this.tempPost = null;
                this.$message({message: msg});
            }
        },
        async doDownLoadPicture(imageUrl, subIndex, subLength) {
            let param = {responseType: 'blob'};
            let decodedUrl = decodeURIComponent(imageUrl);
            console.log('下载图片 %s-%s-%s of %s', this.tempPostEdgesIndex, subLength, subIndex, this.tempPostEdgesSize, decodedUrl);
            let imagePath = '';
            await axios.get(decodedUrl, param).then(res => {
                if (res.status === 200 && typeof (res.data) === 'object' && String(res.data) === '[object Blob]') {
                    let imageBlob = res.data;
                    let imageSha = sha1(decodedUrl);
                    let imageName = 'data-' + this.currentId + '-' + imageSha + '.jpg';
                    console.log('下载图片成功! imageSha=%s', imageSha, res);
                    this.$message({message: [`下载图片成功!`, `${this.tempPostEdgesIndex}-${subLength}-${subIndex} of ${this.tempPostEdgesSize}`]});
                    // 图片下载成功, 上传图片
                    let fd = new FormData();
                    fd.append('file', imageBlob, imageName);
                    return {imageName, imageBlob, imageSha, fd};
                }
                console.log('下载图片失败', res);
                throw res;
            }).then(thenObj => {
                if (typeof (thenObj) === 'object') {
                    let {imageName, imageBlob, imageSha, fd} = thenObj;
                    if (typeof (fd) === 'object' && fd instanceof FormData) {
                        return new Promise(((resolve, reject) => {
                            let gmParam = {
                                url: appConfig.host + 'file',
                                method: 'post',
                                data: fd,
                                overrideMimeType: 'multipart/form-data',
                                onload: (resUp) => {
                                    if (resUp.status === 200 && resUp.responseText.includes('upFile : true')) {
                                        let tempPic = {
                                            name: imageName,
                                            data: imageBlob,
                                            index: this.tempPostEdgesIndex,
                                            subIndex,
                                            subLength,
                                        };
                                        this.tempPicList.push(tempPic);
                                        // /data/instagram/{ userId }/{        download imageUrl sha1        }.jpg
                                        // /data/instagram/1433997056/7929ff968af7ac5dc18b9bf9dde0ee0e08c9b6f5.jpg
                                        imagePath = '/data/instagram/' + this.currentId + '/' + imageSha + '.jpg';
                                        console.log('上传图片成功!', tempPic);
                                        let next = {
                                            name: 'doUpdateEdge'
                                        };
                                        this.doPushNext(next);
                                    } else {
                                        console.log('上传图片失败!');
                                        reject(resUp);
                                        return;
                                    }
                                    resolve();
                                },
                                onerror: (eUp) => {
                                    console.log('上传图片时发生错误!', eUp);
                                    reject(eUp);
                                }
                            };
                            GM_xmlhttpRequest(gmParam);
                        }));
                    } else {
                        console.log('reject fd', fd);
                        throw (fd);
                    }
                } else {
                    console.log('reject fd, thenObj:', thenObj);
                    throw (thenObj);
                }
            }).catch(e => {
                throw(e);
            })
            return imagePath;
        },
        async doGetPost() {
            return new Promise((resolve, reject) => {
                let {runTask} = this.sharedState;
                if (!runTask) {
                    let msg = ['获取getPost异常!', 'runTask:' + typeof (runTask)];
                    console.error(...msg);
                    this.$message({message: msg, level: 'error'});
                    reject(msg);
                    return;
                }
                let failMethod = () => {
                    this.tempPost = null;
                    this.tempPostEdges = null;
                    this.tempPostEdgesSize = 0;
                    this.tempPostEdgesIndex = 0;
                };
                let currentId = this.currentId;
                if (currentId >= 0) {
                    let param = {
                        id: currentId
                    };
                    api.gram.getPostNew(param).then(res => {
                        if (res.status === 200 && res.data.status === 'ok') {
                            this.tempPost = res.data;
                            if (!res.data.data.user) {
                                let msg = ['此用户可能已注销!', `current= ${this.current}, cid= ${currentId}, edges= ${this.tempPostEdgesSize}`];
                                console.log(...msg, res);
                                this.$message({message: msg, level: 'error'});
                                // 执行下一个id
                                this.current = -1;
                                resolve();
                                return;
                            }
                            if (!res.data.data.user.edge_owner_to_timeline_media) {
                                let {config = {}} = this.sharedData;
                                let {viewer = {}} = config;
                                let {username = ''} = viewer;
                                let banMsg = 'currentId=' + currentId + ' ' + username + '用户可能被封号!';
                                let msg = ['获取帖子数据异常', banMsg, `res=${JSON.stringify(res.data)}`];
                                this.banMsg = msg;
                                console.log(...msg, res);
                                this.$message({message: msg, level: 'error'});
                                this.$emit('banMsg', msg);
                                api.banMsg = msg;
                                failMethod();
                                reject(res);
                                return;
                            }
                            this.tempPostEdges = res.data.data.user.edge_owner_to_timeline_media.edges;
                            this.tempPostEdgesSize = this.tempPostEdges.length;
                            let msg = ['下载帖子数据成功!', `current= ${this.current}, cid= ${currentId}, edges= ${this.tempPostEdgesSize}`];
                            console.log(...msg, res);
                            this.$message({message: msg});
                            resolve();
                            return;
                        }
                        if (res.status === 429 && res.data.status === 'fail' && res.data['spam']) {
                            let {config = {}} = this.sharedData;
                            let {viewer = {}} = config;
                            let {username = ''} = viewer;
                            let banMsg = username + '用户被封号!';
                            let msg = ['获取帖子失败', banMsg, `任务结束运行,进度=${this.current}`, JSON.stringify(res.data)];
                            this.banMsg = msg;
                            api.banMsg = msg;
                            console.log(...msg);
                            this.$message({message: msg, level: 'error'});
                            this.$emit('banMsg', msg);
                            this.doStop();
                        }
                        failMethod();
                        reject(res);
                    }).catch(e => {
                        failMethod();
                        reject(e);
                    })
                } else {
                    reject('error currentId is ' + currentId)
                }
            })
        },
        async doGetNextId() {
            return new Promise((resolve, reject) => {
                let {runTask} = this.sharedState;
                if (!runTask) {
                    let msg = ['获取nextId异常!', 'runTask:' + typeof (runTask)];
                    console.error(...msg);
                    this.$message({message: msg, level: 'error'});
                    reject(msg);
                    return;
                }
                if (this.current !== -1) {
                    // 当前id正在执行
                    resolve();
                    return;
                }
                api.ins.next().then(res => {
                    if (res.data && res.data.code === 0) {
                        let {current} = res.data.data;
                        let {nextId} = res.data.data;
                        runTask.current = current;
                        runTask.currentId = nextId;
                        let {total = 0, percentSuccess = 0} = runTask;
                        if (total > 0) {
                            percentSuccess = current / total * 100;
                        }
                        runTask.percentSuccess = percentSuccess;
                        this.current = current;
                        this.currentId = nextId;
                        let msg = ['获取用户id成功!', `cur=${current} cid=${nextId}`];
                        console.log(...msg, res);
                        this.$message({message: msg});
                        console.log('清除邮箱计数');
                        localStorage.setItem("emailCount", "0");
                        resolve();
                        return;
                    }
                    this.currentId = '';
                    reject(res);
                }).catch(e => {
                    this.currentId = '';
                    reject(e);
                })
            })
        },
        async doCheckTask() {
            return new Promise((resolve, reject) => {
                if (this.sharedData) {
                    let {config = {}} = this.sharedData;
                    let {viewer} = config;
                    if (viewer) {
                        if (this.isTaskOk) {
                            resolve();
                            return;
                        }
                        let failMethod = () => {
                            this.isTaskOk = false;
                        };
                        let param = {id: '1001283596'};
                        api.gram.getPostNew(param).then(res => {
                            if (res.status === 200 && res.data.status === 'ok') {
                                console.log('检查任务成功!', res);
                                this.isTaskOk = true;
                                resolve();
                                return;
                            }
                            let msg = '检查任务下载失败!';
                            console.error(msg, res);
                            this.$message({message: msg, level: 'error'});
                            failMethod();
                            reject(res);
                        }).catch(e => {
                            failMethod();
                            reject(e);
                        })
                    } else {
                        reject("没有登录信息,未登录")
                    }
                }
            })
        },
        async doGetSharedData() {
            return new Promise(((resolve, reject) => {
                axios.get('/').then(res => {
                    if (res.status === 200) {
                        if(unsafeWindow._sharedData) {
                            this.sharedData = unsafeWindow._sharedData;
                            console.log('获取用户信息 _sharedData:', unsafeWindow._sharedData);
                            resolve();
                            return;
                        }
                        let jsonStr = res.data.between('>window._sharedData =', ';</script>');
                        if (jsonStr) {
                            let obj = JSON.parse(jsonStr);
                            console.log('获取用户信息 _sharedData:', obj);
                            this.sharedData = obj;
                            resolve();
                            return;
                        }
                    }
                    console.error('响应结果错误', res);
                    reject(res);
                }).catch(e => {
                    reject(e);
                })
            }))
        },
        doCheckUser() {
            if (this.sharedData) {
                let {config = {}} = this.sharedData;
                let {viewer} = config;
                if (viewer && viewer.username) {
                    this.username = viewer.username;
                    localStorage.setItem('lastUser', this.username);
                    this.isUserOk = true;
                    return;
                }
                let next = {
                    name: 'doNextUser'
                };
                this.doPushNext(next);
            }
            this.isUserOk = false;
        },
        async doNextUser() {
            return new Promise(((resolve, reject) => {
                api.user.getAll().then(res => {
                    if (res.data && res.data.code === 0) {
                        let lastUser = localStorage.getItem('lastUser');
                        let allUser = res.data.data;
                        let nextUser = '';
                        let i;
                        for (i in allUser) {
                            // @see <a href="https://blog.csdn.net/qq_27093465/article/details/50802104">IntelliJ IDEA中js代码报如下警告的解决方法</a>
                            if (allUser.hasOwnProperty(i)) {
                                let user = allUser[i];
                                if (user.username) {
                                    if (lastUser) {
                                        if (user.username === lastUser) {
                                            i++;
                                            break;
                                        } else {
                                            nextUser = user.username;
                                            break;
                                        }
                                    } else {
                                        nextUser = user.username;
                                        break;
                                    }
                                }
                            }
                        }
                        if (!nextUser) {
                            if (allUser.length > 0) {
                                i = i >= allUser.length ? i - allUser.length : i;
                                for (; i < allUser.length; i++) {
                                    let user = allUser[i];
                                    if (user.username) {
                                        nextUser = user.username;
                                        break;
                                    }
                                }
                            }
                        }
                        if (!nextUser) {
                            console.error('切换下一个用户失败!', lastUser, res);
                            reject(res);
                            return;
                        }
                        localStorage.setItem('nextUser', nextUser);
                        let next = {
                            name: 'doLogin'
                        };
                        this.doPushNext(next);
                        resolve();
                        return;
                    }
                    reject(res);
                }).catch(e => {
                    reject(e);
                })
            }))
        },
        doLogin() {
            return new Promise(((resolve, reject) => {
                let nextUser = localStorage.getItem('nextUser');
                if (!nextUser) {
                    reject('用户名空');
                    return;
                }
                if (window.location.href !== "https://www.instagram.com/") {
                    window.location.href = "https://www.instagram.com/";
                    return;
                }
            }))
        }
    },
    mounted() {
        console.log('document.cookie', document.cookie);
        console.log(this.$options.name + ' mounted', this.runnerId);
        this.doInit();
    },
    unmounted() {
        console.log(this.$options.name + ' unmounted', this.runnerId);
        this.exit = true;
    },
};
var template = `
<div class="heartbeat">
</div>
`;

let HeartBeat = {
    name: 'HeartBeat',
    components: {},
    template,
    props: {
        list: {
            type: Array,
            default: []
        },
        heartBeat: {
            type: Boolean,
            default: false
        },
    },
    methods: {
        doHeartBeat() {
            return new Promise((resolve, reject) => {
                let result = false;
                api.middle.heartBeat().then(res => {
                    if (res.data && res.data.code === 0) {
                        result = true;
                    } else {
                        console.warn('心跳异常! %s', new Date().fmt(), res)
                    }
                    resolve(result);
                }).catch((e) => {
                    window.e = e;
                    console.error('心跳失败! %s %s', new Date().fmt(), e);
                    resolve(result);
                })
            });
        },
        doNextTimeOut() {
            this.lastTimeOutId = setTimeout(() => {
                clearTimeout(this.lastTimeOutId);
                if (this.exit) {
                    return;
                }
                this.doHeartBeat().then(success => {
                    if (success) {
                        this.timeoutMs = appConfig.heartBeatFixedDelay * 1000;
                        this.lastCostTime = Date.now() - this.lastSuccessTime;
                        this.lastSuccessTime = Date.now();
                        console.log('[%s] 心跳正常! 间隔%s秒', new Date().fmt(), this.lastCostTime / 1000);
                    } else {
                        this.timeoutMs = this.timeoutMs + this.punishMs;
                        console.warn('心跳超时惩罚: %ss', this.timeoutMs / 1000);
                    }
                    this.doNextTimeOut();
                });
            }, this.timeoutMs)
        }
    },
    mounted() {
        console.log('HeartBeat mounted');
        this.doNextTimeOut();
    },
    unmounted() {
        console.log('HeartBeat unMounted, lastTimeOutId: %s', this.lastTimeOutId);
        clearTimeout(this.lastTimeOutId);
        this.exit = true;
        console.log('HeartBeat exit!')
    },
    data() {
        const {useSlots} = Vue;
        return {
            punishMs: appConfig.heartBeatPunish * 1000,
            lastTimeOutId: -1,
            timeoutMs: appConfig.heartBeatFixedDelay * 1000,
            lastSuccessTime: Date.now(),
            lastCostTime: -1,
            exit: false,
        }
    }
};// message.js
var template = `
    <div class="message" v-if="isShow">
        <div :class="'content ' + level">
            <button @click="close" :class="level">X</button>
            <div class="text" v-if="message instanceof Array" v-for="msg in message">
                {{ msg }}
            </div>
            <div class="text" v-else>
                {{ message }}
            </div>
        </div>
    </div>
`;

let Message = {
    name: 'Message',
    data() {
        return {
            list: []
        }
    },
    props: {
        message: {
            // @see <a href="https://v3.cn.vuejs.org/guide/component-props.html#prop-%E9%AA%8C%E8%AF%81">Vue3 Prop 验证</a>
            type: [String, Array], required: true,
        },
        level: {
            type: String, default: 'success'
        },
        duration: {
            type: Number, default: 3,
        },
    },
    setup(props) {
        let {ref} = Vue;
        let isShow = ref(false);
        const show = () => {
            isShow.value = true;
        };
        const close = () => {
            isShow.value = false;
        };
        let {level} = props;
        if ('success' === level) {
            store.state.lastMessageTime = Date.now();
        }
        return {isShow, show, close};
    },
    created() {
        this.show();
    },
    template,
    watch: {
        isShow(nVal, oVal) {

        }
    }
};
var template = `
<div class="crack">
    <div v-if="crackMsg.length > 0" class="msg">
        <div>-- 破解模块 --</div>
        <div v-for="key,index in crackMsg">[{{index+1}}] {{key}}</div>
        <div>破解耗时{{pastTime}}ms</div>
    </div>
    <div :class="'crack-'+!!queryHash" v-else>[破解结果] {{queryHash?'解密成功':'解密失败'}}</div>
    <hr/>
</div>
`;

let Crack = {
    name: 'Crack',
    components: {},
    template,
    data() {
        const {useSlots} = Vue;
        return {
            sharedState: store.state,
            controlSlot: !!useSlots().default,
            crackMsg: [],
            queryHash: '',
            parser: new DOMParser(),
            startTime: new Date().getTime(),
            pastTime: 0,
        }
    },
    methods: {
        async doCrack() {
            let inter=0;
            let count=0;
            console.log('||||||||||||||||||||||||||||||')
            inter=setInterval(()=>{
                count++;
                if(count>=10){
                    clearInterval(inter)
                    console.log('10秒内未破解成功')
                    this.logMsg('解密密码数据失败')
                }
                if(typeof(unsafeWindow)==='undefined'){
                    unsafeWindow=window
                }
                if(unsafeWindow.queryHash){
                    let queryHash = unsafeWindow.queryHash
                    this.saveHash(queryHash)
                    this.logMsg('|||||||||||||| 获取成功 ||||||||||')
                    this.logMsg('已解密密码数据'+queryHash)
                    this.logMsg('-- 破解成功 --')
                    clearInterval(inter)
                }else{
                    this.logMsg('|||||||||||||| 等待破解 ||||||||||'+count)
                }
            },1000)
            // 10秒后清除破解日志
            setTimeout(() => this.crackMsg = [], 10000)
        },
        logMsg(msg) {
            let newMsg = []
            if (Array.isArray(msg)) {
                msg.forEach(e => newMsg.push([new Date().fmt('[yyyy-MM-dd HH:mm:ss]'), e].join(' ')))
            } else {
                newMsg.push([new Date().fmt('[yyyy-MM-dd HH:mm:ss]'), msg].join(' '))
            }
            this.crackMsg.push(...newMsg)
            console.log(...newMsg)
            this.pastTime = new Date().getTime() - this.startTime
        },
        saveHash(queryHash) {
            this.queryHash = queryHash
            this.sharedState.queryHash = this.queryHash
        },
        getForText(src) {
            return new Promise((resolve, reject) => {
                if (!src) {
                    let msg = ['src 地址不能为空 ', src]
                    this.$message({message: msg, level: 'error'})
                    console.log(...msg)
                    reject(msg)
                    return
                }
                axios.get(src).then(res => {
                    if (res.data && res.status === 200) {
                        let html = res.data
                        resolve(html)
                    } else {
                        let msg = ['获取TEXT失败', src]
                        this.$message({message: msg, level: 'error'})
                        console.log(...msg, res)
                        resolve('')
                    }
                }).catch(err => {
                    let msg = ['获取TEXT失败 err', src]
                    this.$message({message: msg, level: 'error'})
                    console.log(...msg, err)
                    resolve('')
                })
            })
        },
        getForHomeHtml() {
            return new Promise((resolve, reject) => {
                axios.get('/').then(res => {
                    if (res.data && res.status === 200) {
                        let html = res.data
                        resolve(html)
                    } else {
                        let msg = ['获取首页HTML失败']
                        this.$message({message: msg, level: 'error'})
                        console.log(...msg, res)
                        resolve('')
                    }
                }).catch(err => {
                    let msg = ['获取首页HTML失败 err']
                    this.$message({message: msg, level: 'error'})
                    console.log(...msg, err)
                    resolve('')
                })
            })
        }
    },
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        proxy.doCrack();
    },
};
var template = `
<div class="task">
    <h3>任务管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    <button @click="doTest">测试</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit && !isTest" @cancel="doCancel" @submit="doSubmit"></Form>
    <Form :data="editModel" :showModel="formTestModel" :schema="schema" v-if="isEdit && isTest" @cancel="doCancel" @submit="doSubmitTest"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doStart(item)" v-if="!item.running">开始</button>
            <button @click="doPause(item)" v-if="item.running&&!item.paused">暂停</button>
            <button @click="doResume(item)" v-if="item.running&&item.paused">恢复</button>
            <button @click="doStop(item)" v-if="item.running">停止</button>
            <button @click="doEdit(item)" :disabled="item.running">修改</button>
            <button @click="doDelete(item)" :disabled="item.running">删除</button>
        </template>
    </List>
    <div v-if="banMsg && banMsg.length > 0">
        <div v-for="key in banMsg">
            {{banMsg[key]}}
        </div>
    </div>
    <Crack></Crack>
    <div v-if="isHealthCheck" class="health">
        <div class="message" :class="'health-'+healthState">
            <span>
            健康监控{{healthState?'正常':'异常'}}
            {{timeUnit(pastSecond)}}
            阀值 {{timeoutStr}}
            </span>
            <button @click="()=>healthCheckPaused=!healthCheckPaused">
            {{healthCheckPaused?'继续':'暂停'}}
            </button>
            <div v-for="key,index in healthMsg">[{{index+1}}] {{key}}</div>
        </div>
    </div>
    <Runner v-if="sharedState.runTask" @update="doUpdate" @banMsg="doBanMsg"></Runner>
</div>
`;


let Task = {
    name: 'Task',
    data() {
        return {
            list: [],
            isEdit: false,
            isTest: false,
            sharedState: store.state,
            // swagger中的schema
            schema: '任务信息',
            model: {
                taskName: '任务-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                threadSize: 1,
                first: '',
                total: '',
                current: '',
                percentSuccess: '',
                timeoutExecTime: '',
                running: false,
                paused: false,
                firstExecTime: '',
                lastExecTime: '',
            },
            modelTest: {
                url: 'https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=4143607182&first=40',
                proxy: 'http://127.0.0.1:1080',
                headers: {
                    'Cookie': 'ds_user_id=51132914782; csrftoken=dNCZEujTqwDLbBPdi6hxExVBy2ymggfE; sessionid=51132914782%3AwJ2vpixHZOofYl%3A8',
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36',
                },
            },
            listShowModel: {
                taskName: '',
                // threadSize: '',
                // first: '',
                total: '',
                current: '',
                // percentSuccess: '',
                running: false,
                paused: false,
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                taskName: '',
                threadSize: 1,
                // first: '',
                // total: '',
                // current: '',
                // percentSuccess: '',
                // running: false,
                // updatedTime: '',
            },
            formTestModel: {
                url: '',
                proxy: '',
                headers: {
                    type: 'map',
                },
            },
            editModel: {},
            heartBeat: false,
            banMsg: '',
            isUnMounted: false,
            isHealthCheck: appConfig.isHealthCheck,
            healthCheckPaused: false,
            pastSecond: 0,
            healthState: true,
            healthMsg: [],
            timeoutStr: -1,
        }
    },
    components: {List, Form, Runner, Crack},
    template,
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        proxy.doGetAll();
        if (proxy.isHealthCheck) {
            proxy.doHealthCheck();
        }
    },
    unmounted() {
        this.isUnMounted = true;
    },
    methods: {
        timeUnit(second) {
            return timeUnit(second);
        },
        async doHealthCheck() {
            let startId = localStorage.getItem("startId")
            this.timeoutStr = this.timeUnit(Math.floor(appConfig.healthCheckIntervalMs / 1000))
            let autoStart = localStorage.getItem("autoStart")
            console.log('healthCheck start startId', startId)
            let isExecAutoStart = false
            while (!this.isUnMounted) {
                if (autoStart && !isExecAutoStart) {
                    let queryHash = this.sharedState.queryHash
                    if (queryHash) {
                        isExecAutoStart = true
                        await this.isHealthCheckPaused()
                        setTimeout(this.doAutoStart, 5000)
                    } else {
                        let msg = ['自动启动任务无法执行', 'queryHash未破解']
                        console.log(...msg);
                        this.$message({message: msg, level: 'error'});
                    }
                }
                await this.isHealthCheckPaused()
                let {lastMessageTime = 0} = this.sharedState;
                let now = Date.now();
                let past = now - lastMessageTime;
                if (past > appConfig.healthCheckIntervalMs) {
                    this.healthState = false;
                    this.sharedState.healthState = this.healthState;
                    this.pastSecond = Math.floor(past / 1000);
                    let msg = ['健康检查超期 将在5秒后尝试重启任务', this.timeUnit(this.pastSecond)];
                    this.healthMsg.push(msg[0])
                    console.log(...msg);
                    this.$message({message: msg, level: 'error'});
                    localStorage.setItem("autoStart", "true")
                    setTimeout(() => {
                        console.log('刷新页面')
                        let nextTimeout = 0;
                        // 停止任务
                        let {runTask} = this.sharedState;
                        if (runTask) {
                            nextTimeout += 1000;
                            this.doStop(runTask)
                        }
                        // 发送邮件
                        nextTimeout += 5000;
                        let emailCount = localStorage.getItem("emailCount") || 0;
                        if (!(emailCount && emailCount >= appConfig.emailCountIgnore)) {
                            emailCount++
                            localStorage.setItem("emailCount", emailCount);
                        } else {
                            this.doSendMail().then(() => {
                                let msg = `${this.timeUnit(nextTimeout / 1000)} 后刷新页面`
                                this.healthMsg.push(msg)
                                console.log(msg)
                                console.log('邮件发送成功, 清除邮箱计数');
                                localStorage.setItem("emailCount", "0");
                                // 刷新页面
                                setTimeout(() => history.go(0), nextTimeout)
                            })
                        }
                    }, 10000)
                    await this.sleep(20000);
                } else {
                    this.healthState = true;
                    this.sharedState.healthState = this.healthState;
                }
                this.pastSecond = Math.floor(past / 1000);
                await this.sleep(1000);
            }
            console.log('healthCheck stopped')
        },
        async sleep(sleepMs = this.sleepTimeMs) {
            return new Promise(((resolve, reject) => {
                setTimeout(() => {
                    resolve()
                }, sleepMs)
            }))
        },
        async isHealthCheckPaused() {
            while (this.healthCheckPaused) {
                await this.sleep(300)
            }
        },
        async doSendMail() {
            let emailList = [];
            await this.$api.notifyEmail.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    emailList = res.data.data
                    emailList = emailList
                        .filter(mail => mail.enabled && mail.email)
                        .map(mail => mail.email)
                    let msg = `获取邮件列表成功! 待通知邮箱(${emailList.length}个) ${emailList}`
                    this.healthMsg.push(msg)
                    console.log(msg, res)
                } else {
                    let msg = `获取邮件列表失败!`
                    this.healthMsg.push(msg)
                    console.log(msg, res, emailList)
                }
            }).catch(err => {
                let msg = `获取邮件列表异常!`
                this.healthMsg.push(msg)
                console.log('notifyEmail err', err)
            })
            let mailDto = {
                content: `${new Date().fmt('[爬虫告警][yyyy-MM-dd HH:mm:ss] ')}爬虫任务健康检查失败, 将在5秒后尝试重启任务; 请及时进行检查!\n[检查周期] ${this.timeoutStr}`,
                title: `《${new Date().fmt('[爬虫告警][yyyy-MM-dd HH:mm:ss] ')}任务健康检查告警》`,
                to: ""
            };
            this.healthMsg.push(`即将发送告警邮件`)
            this.healthMsg.push(`[标题] ${mailDto.title}`)
            this.healthMsg.push(`[内容] ${mailDto.content}`)
            for (let i = 0; i < emailList.length; i++) {
                const email = emailList[i];
                let msg = `尝试给${email}发送邮件`
                this.healthMsg.push(msg)
                let emailObj = {
                    content: mailDto.content,
                    title: mailDto.title,
                    to: email,
                }
                await this.$api.eMail.send(emailObj).then(res => {
                    if (res.data && res.data.code === 0) {
                        let msg = `给${email}发送邮件成功!`
                        this.healthMsg.push(msg)
                        console.log(msg, res)
                    } else {
                        let {data = {msg: ''}} = res
                        let msg = `给${email}发送邮件失败!${data.msg}`
                        this.healthMsg.push(msg)
                        console.log(msg, res)
                    }
                }).catch(err => {
                    let msg = `给${email}发送邮件异常!`
                    this.healthMsg.push(msg)
                    console.log('发送邮件异常!', err)
                })
            }
        },
        doAutoStart() {
            let msg = '触发自动启动任务'
            this.healthMsg.push(msg)
            console.log(msg)
            let startId = localStorage.getItem("startId")
            let find = this.list.filter(e => e.id === startId)
            let item = null;
            if (find && find.length > 0) {
                item = find[0];
            } else {
                if (this.list && this.list.length > 0) {
                    item = this.list[0]
                }
            }
            if (item) {
                let {taskName} = item
                let msg = ['健康检查 启动任务', taskName]
                this.healthMsg.push(...msg)
                console.log('健康检查 启动任务', item)
                this.doStart(item)
            } else {
                let msg = ['健康检查', '无可用启动任务']
                this.healthMsg.push(...msg)
                console.log(...msg, this.list)
                this.$message({message: msg, level: 'error'})
            }
        },
        doRefresh() {
            this.$api.task.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '口调用接失败';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.task.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let {runTask, healthState = true} = this.sharedState;
                    if (!runTask) {
                        let list = deepClone(this.list)
                        list.sort(item => item.running ? -1 : 1).forEach(item => {
                            if (!healthState) {
                                healthState = true;
                                this.doStart(item);
                            }
                        })
                    }
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.task.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.doUpdate(this.editModel);
        },
        doUpdate(item) {
            if (!item) {
                let msg = '保存失败!';
                console.error(msg, item);
                this.$message({message: msg, level: 'error'});
                return;
            }
            this.$api.task.update(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doBanMsg(msg = null) {
            this.banMsg = msg;
        },
        doCancel() {
            this.isEdit = false;
            this.isTest = false;
        },
        doStart(item) {
            let {runTask} = this.sharedState;
            if (runTask) {
                let msg = "已有任务在运行 " + runTask.taskName;
                console.warn('[%s] '.replace('%s', new Date().fmt()) + msg);
                this.$message({message: ['已有任务在运行', runTask.taskName], level: 'error'});
            } else {
                this.sharedState.runTask = item;
                let {id} = item;
                localStorage.setItem("startId", id)
                item.running = true;
                let msg = [`任务启动`, item.taskName];
                console.log(...msg);
                this.$message({message: msg});
            }
        },
        doStop(item) {
            item.running = false;
            item.paused = false;
            let {runTask} = this.sharedState;
            if (runTask && runTask.running && runTask.id === item.id) {
                runTask.running = false;
                runTask.paused = false;
                this.sharedState.runTask = null;
            } else {
                // 非运行任务强行停止
                this.doUpdate(item);
            }
        },
        doPause(item) {
            let {runTask} = this.sharedState;
            if (runTask && !runTask.paused && runTask.id === item.id) {
                item.paused = true;
                runTask.paused = true;
            }
        },
        doResume(item) {
            let {runTask} = this.sharedState;
            if (runTask && runTask.paused && runTask.id === item.id) {
                item.paused = false;
                runTask.paused = false;
            }
        },
        doTest(item) {
            this.editModel = deepClone(this.modelTest);
            this.isEdit = true;
            this.isTest = true;
        },
        doSubmitTest() {
            this.$api.task.test(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '测试成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg + '\n' + res.data.data, duration: 10});
                    // this.isEdit = false;
                    // this.doGetAll()
                } else {
                    let msg = '测试失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '测试失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
    },
    watch: {
        list(newVal, oldVal) {
            console.log('list changed')
        },
        sharedState(newVal, oldVal) {
            console.log('sharedState changed')
        },
    },
};
var template = `
<div class="proxy">
    <h3>代理管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let ProxyCom = {
    name: 'ProxyCom',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '代理配置',
            model: {
                proxyName: '代理-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                url: '',
                ip: '',
                port: 0,
                password: '',
                method: '',
                enabled: true,
            },
            listShowModel: {
                proxyName: '代理-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                url: {
                    type: 'text',
                    maxLength: 15,
                },
                ip: '',
                port: 0,
                password: '',
                method: '',
                enabled: {
                    success: true,
                },
                available: {
                    success: true,
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                proxyName: '代理-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                url: '',
                ip: '',
                port: 0,
                password: '',
                method: '',
                enabled: {
                    type: 'boolean',
                },
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('proxy mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.proxy.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.proxy.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.proxy.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.proxy.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div class="user">
    <h3>用户管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let User = {
    name: 'User',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '用户信息',
            model: {
                username: '用户-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                password: '',
                cookies: '',
                enabled: true,
                baned: false,
            },
            listShowModel: {
                username: '用户-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                password: '',
                cookies: '',
                enabled: {
                    success: true,
                },
                baned: false,
                available: {
                    success: true,
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                username: '用户-' + new Date().fmt('yyyy年MM月dd日-HH-mm-ss'),
                password: '',
                cookies: '',
                enabled: {
                    type: 'boolean',
                },
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('user mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.user.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.user.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.user.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.user.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div class="notify-email">
    <h3>通知邮箱管理</h3>
    <button @click="doAdd" v-if="!isEdit">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let NotifyEmail = {
    name: 'NotifyEmail',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '通知邮箱信息',
            model: {
                email: 'mail@admin.com',
                enabled: true,
            },
            listShowModel: {
                email: 'mail@admin.com',
                enabled: {},
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            formShowModel: {
                email: 'mail@admin.com',
                enabled: {
                    type: 'boolean',
                },
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('user mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.notifyEmail.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.notifyEmail.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.notifyEmail.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.notifyEmail.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div class="config">
    <h3>设置管理</h3>
    <button @click="doAdd" v-if="!isEdit && list.length === 0">新增</button>
    <button @click="doCancel" v-if="isEdit">取消</button>
    <button @click="doRefresh">刷新</button>
    
    <Form :data="editModel" :showModel="formShowModel" :schema="schema" v-if="isEdit" @cancel="doCancel" @submit="doSubmit"></Form>
    <List :list="list" :showModel="listShowModel" :schema="schema">
        <template v-slot="{item}">
            <button @click="doEdit(item)">修改</button>
            <button @click="doDelete(item)">删除</button>
        </template>
    </List>
</div>
`;


let Config = {
    name: 'Config',
    data() {
        return {
            list: [],
            isEdit: false,
            schema: '系统配置',
            model: {
                threadSize: '',
                sleepTimeMs: '',
                sleepTaskMs: '',
                proxyCheckUrl: '',
                userCheckUrl: '',
                userCheck: false,
                defaultUserAgent: '',
                autoCreateTask: false,
                autoCreateTaskInterval: 0,
            },
            listShowModel: {
                threadSize: '',
                sleepTimeMs: '',
                sleepTaskMs: '',
                proxyCheckUrl: {
                    type: 'text',
                    maxLength: 15,
                },
                userCheckUrl: {
                    type: 'text',
                    maxLength: 15,
                },
                userCheck: false,
                defaultUserAgent: {
                    type: 'text',
                    maxLength: 15,
                },
                autoCreateTask: false,
                autoCreateTaskInterval: 0,
            },
            formShowModel: {
                threadSize: '',
                sleepTimeMs: '',
                sleepTaskMs: '',
                proxyCheckUrl: '',
                userCheckUrl: '',
                userCheck: {
                    type: 'boolean',
                },
                defaultUserAgent: '',
                autoCreateTask: {
                    type: 'boolean',
                },
                autoCreateTaskInterval: 0,
            },
            editModel: {}
        }
    },
    components: {List, Form},
    template,
    mounted: () => {
        console.log('config mounted');
        const {proxy} = Vue.getCurrentInstance();
        proxy.doGetAll();
    },
    methods: {
        doRefresh() {
            this.$api.config.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                    let msg = '刷新成功';
                    this.$message({message: msg});
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doGetAll() {
            this.$api.config.getAll().then(res => {
                if (res.data && res.data.code === 0) {
                    this.list = res.data.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doAdd() {
            this.editModel = deepClone(this.model);
            this.isEdit = true;
        },
        doEdit(item) {
            this.editModel = deepClone(item);
            this.isEdit = true;
        },
        doDelete(item) {
            this.$api.config.delete(item).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '删除成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '删除失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '删除失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doSubmit() {
            this.$api.config.update(this.editModel).then(res => {
                if (res.data && res.data.code === 0) {
                    let msg = '保存成功!';
                    console.log(msg, res.data);
                    this.$message({message: msg});
                    this.isEdit = false;
                    this.doGetAll()
                } else {
                    let msg = '保存失败!';
                    console.error(msg, res.data);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '保存失败!';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        },
        doCancel() {
            this.isEdit = false;
        },
        doStart() {
            alert('start')
        }
    }
};
var template = `
<div v-is="'style'">
    {{cssData}}
</div>
`;


let Css = {
    name: 'Css',
    data() {
        return {
            cssData: `
        .task h2 {
            font-size: 14px;
            color: yellow;
        }
    `
        }
    },
    components: {},
    template,
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        // proxy.doGetCss();
        proxy.cssData = `
#app-main .nav h1 {
    color: red;
}

#app-main .menu a {
    margin: 5px;
}

#app-main button {
    margin: 5px;
}

/*    message    */
#app-message {
    position: absolute;
    right: 5%;
    top: 5%;
    z-index: 99999;
}

#app-message .message .content {
    background: rgba(255, 255, 255, 0.7);
    border: solid 1px rgba(0, 0, 0, 0.7);
    border-radius: 10px;
    padding: 5px;
    margin: 5px;
    min-width: 120px;
    text-align: center;
    word-break: break-all;
    max-width: 200px;

}

#app-message .message .text {
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
    margin-top: -2px;
}

#app-message .message button {
    border-radius: 10px;
    border: solid 1px black;
    height: 22px;
    width: 22px;
    position: relative;
    right: 0px;
    margin-top: -1px;
    float: right;
    margin-bottom: -2px;
}

#app-message .message .error {
    border-color: red;
    color: red;
}

#app-message .message .success {
    border-color: green;
    color: green;
}

/*config*/
#app-main .config h2 {
    font-size: 14px;
}

/*ins-crack*/
#app-main .ins-crack h2 {
    font-size: 14px;
}

#app-main .ins-crack textarea {
    min-height: 200px;
    width: 85%;
}

/*form*/
#app-main .form {
    margin-bottom: 5px;
    max-width: 400px;
    border: solid 1px;
}

#app-main .form .label {
    margin: 5px;
    float: left;
    max-width: 150px;
    min-width: 100px;
    padding: 1px;
}

#app-main .form .input {
    margin: 5px;
    float: right;
    width: 200px;
}

#app-main .form button {
    float: right;
}

#app-main .form .key {
    width: 64px;
}

#app-main .form .value {
    width: 84px;
}

#app-main .form .del {
    margin-left: -5px;
    margin-top: 2px;
    float: inherit;
    width: 22px;
}

#app-main .form .add {
    width: 22px;
    margin-right: 6px;
}

/*list*/
#app-main .list table {
    border-right: 1px solid;
    border-bottom: 1px solid;
}

#app-main .list table td, th {
    border-left: 1px solid;
    border-top: 1px solid;
    word-break: break-all;
}

#app-main .list table .green {
    background-color: green;
}

/*proxy*/
#app-main .proxy h2 {
    font-size: 14px;
}

/*task*/
#app-main .task h2 {
    font-size: 14px;
}

/*user*/
#app-main .user h2 {
    font-size: 14px;
}

/*user*/
#app-main .notify-email h2 {
    font-size: 14px;
}

/*runner*/
#app-main .runner hr {
    border: none;
}

/*task.health*/
#app-main .health .message {
    border: none;
}

#app-main .health .health-true {
    color: green;
}

#app-main .health .health-false {
    color: red;
}

#app-main .crack hr {
    border-style: solid;
    border-width: 1px 0px 0px 0px;
}

#app-main .crack .true {
    color: green;
}

#app-main .crack .crack-false {
    color: red;
}

`;
    },
    methods: {
        doGetCss() {
            this.$api.style.getApp().then(res => {
                if (res.status === 200 && res.data) {
                    this.cssData = res.data;
                } else {
                    let msg = '接口调用失败';
                    console.error(msg, res);
                    this.$message({message: msg, level: 'error'});
                }
            }).catch(e => {
                let msg = '接口调用失败';
                console.error(msg, e);
                this.$message({message: msg, level: 'error'});
            })
        }
    }
};// <!-- 展示模板 -->
var template = `
<div>
    <div class="nav">
        <h1 class="title">{{appName}}!</h1>
        <p class="menu">
            <!--使用 router-link 组件进行导航 -->
            <!--通过传递 \`to\` 来指定链接 -->
            <!--\`<router-link>\` 将呈现一个带有正确 \`href\` 属性的 \`<a>\` 标签-->
            <router-link to="/">任务管理</router-link>
            <router-link to="/proxy">代理配置</router-link>
            <router-link to="/user">用户配置</router-link>
            <router-link to="/config">系统配置</router-link>
            <router-link to="/notify-email">通知邮箱</router-link>
        </p>
    </div>
    
    <!-- 路由出口 -->
    <!-- 路由匹配到的组件将渲染在这里 -->
    <router-view></router-view>
    <!-- <a href="https://www.jianshu.com/p/b0c16bab3388">vue3使用is和v-is</a> -->
    <div v-is="'style'" scoped>
    </div>
    <Css></Css>
    <HeartBeat></HeartBeat>
</div>
`;
// <!-- Vue 代码 -->


let App = {
    name: 'App',
    data: () => {
        return {
            sharedState: store.state,
            appName: 'Instagram Crawler Admin',
            book: 'book1'
        }
    },
    components: {
        Css, HeartBeat
    },
    template: template,
    mounted: () => {
        const {proxy} = Vue.getCurrentInstance();
        console.log('%s mounted', proxy.$options.name);
        proxy.initApiDocs();
    },
    methods: {
        initApiDocs() {
            api.swagger.apiDocs().then(res => {
                if (res.status === 200 && res.data && res.data.components) {
                    this.sharedState.apiDocs = res.data;
                    console.log('load apiDocs', this.sharedState.apiDocs);
                }
            }).catch(e => {
                console.log('load apiDocs error', e)
            });
        }
    }
};
// 1. 定义路由组件.
// 也可以从其他文件导入
// const Task = { template: '<div>Task Page</div>' };
// const Proxy = { template: '<div>Proxy Page</div>' };

// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
    {path: '/', component: Task},
    {path: '/proxy', component: ProxyCom},
    {path: '/config', component: Config},
    {path: '/user', component: User},
    {path: '/notify-email', component: NotifyEmail},
];

